Giter Site home page Giter Site logo

dingding-automatic-clock-in's Introduction

⏰ DingDing-Automatic-Clock-in

📖 简介

钉钉全自动打卡 + 远程打卡脚本,无需 root,基于 auto.js,适用于蓝牙考勤机。

💥 功能

  • 定时打卡
  • 远程打卡
  • 发送考勤结果

⚙️ 工具

  • auto.js
  • Tasker
  • 一款通讯应用(示例脚本中使用的是 QQ / 网易邮箱大师 / ServerChan / PushDeer,彼此互为备用方案)

💡 原理

通过 auto.js 脚本监听本机通知,在 Tasker 中创建定时任务,发出通知,或在另一设备上发送消息到本机,即可触发脚本中的打卡进程,实现定时打卡和远程打卡。

image

同理,监听到钉钉发出的打卡成功通知后,将通知文本通过 QQ消息 或 邮件正文 发送,实现发送考勤结果的功能。

📝 脚本

/*
 * @Author: George Huan
 * @Date: 2020-08-03 09:30:30
 * @LastEditTime: 2022-03-26 10:56:25
 * @Description: DingDing-Automatic-Clock-in (Run on AutoJs)
 * @URL: https://github.com/georgehuan1994/DingDing-Automatic-Clock-in
 */

const ACCOUNT = "钉钉账号"
const PASSWORD = "钉钉密码"

const QQ =              "用于接收打卡结果的QQ号"
const EMAILL_ADDRESS =  "用于接收打卡结果的邮箱地址"
const SERVER_CHAN =     "Server酱发送密钥"
const PUSH_DEER =       "PushDeer发送密钥"

const PUSH_METHOD = {QQ: 1, Email: 2, ServerChan: 3, PushDeer: 4}

// 默认通信方式:
// PUSH_METHOD.QQ -- QQ
// PUSH_METHOD.Email -- Email 
// PUSH_METHOD.ServerChan -- Server酱
// PUSH_METHOD.PushDeer -- Push Deer
var DEFAULT_MESSAGE_DELIVER = PUSH_METHOD.QQ;

const PACKAGE_ID_QQ = "com.tencent.mobileqq"                // QQ
const PACKAGE_ID_DD = "com.alibaba.android.rimet"           // 钉钉
const PACKAGE_ID_XMSF = "com.xiaomi.xmsf"                   // 小米推送服务
const PACKAGE_ID_TASKER = "net.dinglisch.android.taskerm"   // Tasker
const PACKAGE_ID_MAIL_163 = "com.netease.mail"              // 网易邮箱大师
const PACKAGE_ID_MAIL_ANDROID = "com.android.email"         // 系统内置邮箱
const PACKAGE_ID_PUSHDEER = "com.pushdeer.os"               // Push Deer

const LOWER_BOUND = 1 * 60 * 1000 // 最小等待时间:1min
const UPPER_BOUND = 5 * 60 * 1000 // 最大等待时间:5min

// 执行时的屏幕亮度(0-255), 需要"修改系统设置"权限
const SCREEN_BRIGHTNESS = 20    

// 是否过滤通知
const NOTIFICATIONS_FILTER = true

// PackageId白名单
const PACKAGE_ID_WHITE_LIST = [PACKAGE_ID_QQ,PACKAGE_ID_DD,PACKAGE_ID_XMSF,PACKAGE_ID_MAIL_163,PACKAGE_ID_TASKER,PACKAGE_ID_PUSHDEER]

// 公司的钉钉CorpId, 获取方法见 2020-09-24 更新日志。如果只加入了一家公司, 可以不填
const CORP_ID = "" 

// 锁屏意图, 配合 Tasker 完成锁屏动作, 具体配置方法见 2021-03-09 更新日志
const ACTION_LOCK_SCREEN = "autojs.intent.action.LOCK_SCREEN"

// 监听音量+键, 开启后无法通过音量+键调整音量, 按下音量+键:结束所有子线程
const OBSERVE_VOLUME_KEY = true

const WEEK_DAY = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday",]


// =================== ↓↓↓ 主线程:监听通知 ↓↓↓ ====================

var currentDate = new Date()

// 是否暂停定时打卡
var suspend = false

// 本次打开钉钉前是否需要等待
var needWaiting = true

// 运行日志路径
var globalLogFilePath = "/sdcard/脚本/Archive/" + getCurrentDate() + "-log.txt"

// 检查无障碍权限
auto.waitFor("normal")

// 检查Autojs版本
requiresAutojsVersion("4.1.0")

// 创建运行日志
console.setGlobalLogConfig({
    file: "/sdcard/脚本/Archive/" + getCurrentDate() + "-log.txt"
});

// 监听本机通知
events.observeNotification()    
events.on("notification", function(n) {
    notificationHandler(n)
});

events.setKeyInterceptionEnabled("volume_up", OBSERVE_VOLUME_KEY)

if (OBSERVE_VOLUME_KEY) {
    events.observeKey()
};
    
// 监听音量+键
events.onKeyDown("volume_up", function(event){
    threads.shutDownAll()
    device.setBrightnessMode(1)
    device.cancelKeepingAwake()
    toast("已中断所有子线程!")

    // 可以在此调试各个方法
    // doClock()
    // sendQQMsg("测试文本")
    // sendEmail("测试主题", "测试文本", null)
    // sendServerChan(测试主题, 测试文本)
    // sendPushDeer(测试主题, 测试文本)
});

toastLog("监听中, 请在日志中查看记录的通知及其内容")

// =================== ↑↑↑ 主线程:监听通知 ↑↑↑ =====================



/**
 * @description 处理通知
 */
function notificationHandler(n) {
    
    var packageId = n.getPackageName()  // 获取通知包名
    var abstract = n.tickerText         // 获取通知摘要
    var text = n.getText()              // 获取通知文本
    
    // 过滤 PackageId 白名单之外的应用所发出的通知
    if (!filterNotification(packageId, abstract, text)) { 
        return;
    }

    // 监听摘要为 "定时打卡" 的通知, 不一定要从 Tasker 中发出通知, 日历、定时器等App均可实现
    if (abstract == "定时打卡" && !suspend) { 
        needWaiting = true
        threads.shutDownAll()
        threads.start(function(){
            doClock()
        })
        return;
    }

    switch(text) {
        
        case "打卡": // 监听文本为 "打卡" 的通知
            needWaiting = false
            threads.shutDownAll()
            threads.start(function(){
                doClock()
            })
            break;

        case "查询": // 监听文本为 "查询" 的通知
            threads.shutDownAll()
            threads.start(function(){
                switch(DEFAULT_MESSAGE_DELIVER) {
                    case PUSH_METHOD.QQ:
                        sendQQMsg(getStorageData("dingding", "clockResult"))
                       break;
                    case PUSH_METHOD.Email:
                        sendEmail("考勤结果", getStorageData("dingding", "clockResult"), null)
                       break;
                    case PUSH_METHOD.ServerChan:
                        sendServerChan("考勤结果", getStorageData("dingding", "clockResult"))
                       break;
                    case PUSH_METHOD.PushDeer:
                        sendPushDeer("考勤结果", getStorageData("dingding", "clockResult"))
                       break;
                }
            })
            break;

        case "暂停": // 监听文本为 "暂停" 的通知
            suspend = true
            console.warn("暂停定时打卡")
            threads.shutDownAll()
            threads.start(function(){
                switch(DEFAULT_MESSAGE_DELIVER) {
                    case PUSH_METHOD.QQ:
                        sendQQMsg("修改成功, 已暂停定时打卡功能")
                       break;
                    case PUSH_METHOD.Email:
                        sendEmail("修改成功", "已暂停定时打卡功能", null)
                       break;
                    case PUSH_METHOD.ServerChan:
                        sendServerChan("修改成功", "已暂停定时打卡功能")
                       break;
                    case PUSH_METHOD.PushDeer:
                        sendPushDeer("修改成功", "已暂停定时打卡功能")
                       break;
                }
            })
            break;

        case "恢复": // 监听文本为 "恢复" 的通知
            suspend = false
            console.warn("恢复定时打卡")
            threads.shutDownAll()
            threads.start(function(){
                switch(DEFAULT_MESSAGE_DELIVER) {
                    case PUSH_METHOD.QQ:
                        sendQQMsg("修改成功, 已恢复定时打卡功能")
                       break;
                    case PUSH_METHOD.Email:
                        sendEmail("修改成功", "已恢复定时打卡功能", null)
                       break;
                    case PUSH_METHOD.ServerChan:
                        sendServerChan("修改成功", "已恢复定时打卡功能")
                       break;
                    case PUSH_METHOD.PushDeer:
                        sendPushDeer("修改成功", "已恢复定时打卡功能")
                       break;
                }
            })
            break;

        case "日志": // 监听文本为 "日志" 的通知
            threads.shutDownAll()
            threads.start(function(){
                sendEmail("获取日志", globalLogFilePath, globalLogFilePath)
            })
            break;

        default:
            break;
    }

    if (text == null) 
    return;
    
    // 监听钉钉返回的考勤结果
    if (packageId == PACKAGE_ID_DD && text.indexOf("考勤打卡") >= 0) { 
        setStorageData("dingding", "clockResult", text)
        threads.shutDownAll()
        threads.start(function() {
            switch(DEFAULT_MESSAGE_DELIVER) {
                case PUSH_METHOD.QQ:
                    sendQQMsg(text)
                   break;
                case PUSH_METHOD.Email:
                    sendEmail("考勤结果", text, cameraFilePath)
                   break;
                case PUSH_METHOD.ServerChan:
                    sendServerChan("考勤结果", text)
                   break;
                case PUSH_METHOD.PushDeer:
                    sendPushDeer("考勤结果", text)
                   break;
           }
        })
        return;
    }
}


/**
 * @description 打卡流程
 */
function doClock() {

    currentDate = new Date()
    console.log("本地时间: " + getCurrentDate() + " " + getCurrentTime())
    console.log("开始打卡流程!")

    brightScreen()      // 唤醒屏幕
    unlockScreen()      // 解锁屏幕
    holdOn()            // 随机等待
    signIn()            // 自动登录
    handleLate()        // 处理迟到
    attendKaoqin()      // 考勤打卡

    if (currentDate.getHours() <= 12) 
    clockIn()           // 上班打卡
    else 
    clockOut()          // 下班打卡
    
    lockScreen()        // 关闭屏幕
}


/**
 * @description 发送邮件流程
 * @param {string} title 邮件主题
 * @param {string} message 邮件正文
 * @param {string} attachFilePath 要发送的附件路径
 */
function sendEmail(title, message, attachFilePath) {

    console.log("开始发送邮件流程!")

    brightScreen()      // 唤醒屏幕
    unlockScreen()      // 解锁屏幕

    if(attachFilePath != null && files.exists(attachFilePath)) {
        console.info(attachFilePath)
        app.sendEmail({
            email: [EMAILL_ADDRESS], subject: title, text: message, attachment: attachFilePath
        })
    }
    else {
        console.error(attachFilePath)
        app.sendEmail({
            email: [EMAILL_ADDRESS], subject: title, text: message
        })
    }
    
    console.log("选择邮件应用")
    waitForActivity("com.android.internal.app.ChooserActivity") // 等待选择应用界面弹窗出现, 如果设置了默认应用就注释掉
    
    var emailAppName = app.getAppName(PACKAGE_ID_MAIL_163)
    if (null != emailAppName) {
        if (null != textMatches(emailAppName).findOne(1000)) {
            btn_email = textMatches(emailAppName).findOnce().parent()
            btn_email.click()
        }
    }
    else {
        console.error("不存在应用: " + PACKAGE_ID_MAIL_163)
        lockScreen()
        return;
    }

    // 网易邮箱大师
    var versoin = getPackageVersion(PACKAGE_ID_MAIL_163)
    console.log("应用版本: " + versoin)
    var sp = versoin.split(".")
    if (sp[0] == 6) {
        // 网易邮箱大师 6
        waitForActivity("com.netease.mobimail.activity.MailComposeActivity")
        id("send").findOne().click()
    }
    else {
        // 网易邮箱大师 7
        waitForActivity("com.netease.mobimail.module.mailcompose.MailComposeActivity")
        var input_address = id("input").findOne()
        if (null == input_address.getText()) {
            input_address.setText(EMAILL_ADDRESS)
        }
        id("iv_arrow").findOne().click()
        sleep(1000)
        id("img_send_bg").findOne().click()
    }
    
    // 内置电子邮件
    // waitForActivity("com.kingsoft.mail.compose.ComposeActivity")
    // id("compose_send_btn").findOne().click()

    console.log("正在发送邮件...")
    
    home()
    sleep(2000)
    lockScreen()    // 关闭屏幕
}


/**
 * @description 发送QQ消息
 * @param {string} message 消息内容
 */
function sendQQMsg(message) {

    console.log("发送QQ消息")
    
    brightScreen()      // 唤醒屏幕
    unlockScreen()      // 解锁屏幕

    app.startActivity({ 
        action: "android.intent.action.VIEW", 
        data:"mqq://im/chat?chat_type=wpa&version=1&src_type=web&uin=" + QQ, 
        packageName: "com.tencent.mobileqq", 
    });
    
    // waitForActivity("com.tencent.mobileqq.activity.SplashActivity")

    id("input").findOne().setText(message)
    id("fun_btn").findOne().click()

    home()
    sleep(1000)
    lockScreen()    // 关闭屏幕
}


/**
 * @description ServerChan推送
 * @param {string} title 标题
 * @param {string} message 消息
 */
 function sendServerChan(title, message) {

    console.log("向 ServerChan 发起推送请求")

    url = "https://sctapi.ftqq.com/" + SERVER_CHAN + ".send";

    res = http.post(encodeURI(url), {
        "title": title,
        "desp": message
    });

    console.log(res)
    sleep(1000)
    lockScreen()    // 关闭屏幕
}


/**
 * @description PushDeer推送
 * @param {string} title 标题
 * @param {string} message 消息
 */
 function sendPushDeer(title, message) {

    console.log("向 PushDeer 发起推送请求")

    url = "https://api2.pushdeer.com/message/push"

    res = http.post(encodeURI(url), {
        "pushkey": PUSH_DEER,
        "text": title,
        "desp": message,
        "type": "markdown",
    });

    console.log(res)
    sleep(1000)
    lockScreen()    // 关闭屏幕
}


/**
 * @description 唤醒设备
 */
function brightScreen() {

    console.log("唤醒设备")
    
    device.setBrightnessMode(0) // 手动亮度模式
    device.setBrightness(SCREEN_BRIGHTNESS)
    device.wakeUpIfNeeded() // 唤醒设备
    device.keepScreenOn()   // 保持亮屏
    sleep(1000) // 等待屏幕亮起
    
    if (!device.isScreenOn()) {
        console.warn("设备未唤醒, 重试")
        device.wakeUpIfNeeded()
        brightScreen()
    }
    else {
        console.info("设备已唤醒")
    }
    sleep(1000)
}


/**
 * @description 解锁屏幕
 */
function unlockScreen() {

    console.log("解锁屏幕")
    
    if (isDeviceLocked()) {

        gesture(
            320, // 滑动时间:毫秒
            [
                device.width  * 0.5,    // 滑动起点 x 坐标:屏幕宽度的一半
                device.height * 0.9     // 滑动起点 y 坐标:距离屏幕底部 10% 的位置, 华为系统需要往上一些
            ],
            [
                device.width / 2,       // 滑动终点 x 坐标:屏幕宽度的一半
                device.height * 0.1     // 滑动终点 y 坐标:距离屏幕顶部 10% 的位置
            ]
        )

        sleep(1000) // 等待解锁动画完成
        home()
        sleep(1000) // 等待返回动画完成
    }

    if (isDeviceLocked()) {
        console.error("上滑解锁失败, 请按脚本中的注释调整 gesture(time, [x1,y1], [x2,y2]) 方法的参数!")
        return;
    }
    console.info("屏幕已解锁")
}


/**
 * @description 随机等待
 */
function holdOn(){

    if (!needWaiting) {
        return;
    }

    var randomTime = random(LOWER_BOUND, UPPER_BOUND)
    toastLog(Math.floor(randomTime / 1000) + "秒后启动" + app.getAppName(PACKAGE_ID_DD) + "...")
    sleep(randomTime)
}


/**
 * @description 启动并登陆钉钉
 */
function signIn() {

    app.launchPackage(PACKAGE_ID_DD)
    console.log("正在启动" + app.getAppName(PACKAGE_ID_DD) + "...")

    setVolume(0) // 设备静音

    sleep(10000) // 等待钉钉启动

    if (currentPackage() == PACKAGE_ID_DD &&
        currentActivity() == "com.alibaba.android.user.login.SignUpWithPwdActivity") {
        console.info("账号未登录")

        var account = id("et_phone_input").findOne()
        account.setText(ACCOUNT)
        console.log("输入账号")

        var password = id("et_pwd_login").findOne()
        password.setText(PASSWORD)
        console.log("输入密码")
        
        var privacy = id("cb_privacy").findOne()
        privacy.click()
        console.log("同意隐私协议")
        
        var btn_login = id("btn_next").findOne()
        btn_login.click()
        console.log("正在登陆...")

        sleep(3000)
    }

    if (currentPackage() == PACKAGE_ID_DD &&
        currentActivity() != "com.alibaba.android.user.login.SignUpWithPwdActivity") {
        console.info("账号已登录")
        sleep(1000)
    }
}


/**
 * @description 处理迟到打卡
 */
function handleLate(){
   
    if (null != textMatches("迟到打卡").clickable(true).findOne(1000)) {
        btn_late = textMatches("迟到打卡").clickable(true).findOnce() 
        btn_late.click()
        console.warn("迟到打卡")
    }
    if (null != descMatches("迟到打卡").clickable(true).findOne(1000)) {
        btn_late = descMatches("迟到打卡").clickable(true).findOnce() 
        btn_late.click()
        console.warn("迟到打卡")
    }
}


/**
 * @description 使用 URL Scheme 进入考勤界面
 */
function attendKaoqin(){

    var url_scheme = "dingtalk://dingtalkclient/page/link?url=https://attend.dingtalk.com/attend/index.html"

    if(CORP_ID != "") {
        url_scheme = url_scheme + "?corpId=" + CORP_ID
    }

    var a = app.intent({
        action: "VIEW",
        data: url_scheme,
        //flags: [Intent.FLAG_ACTIVITY_NEW_TASK]
    });
    app.startActivity(a);
    console.log("正在进入考勤界面...")
    
    textContains("申请").waitFor()
    console.info("已进入考勤界面")
    sleep(1000)
}


/**
 * @description 上班打卡 
 */
function clockIn() {

    console.log("上班打卡...")

    if (null != textContains("已打卡").findOne(1000)) {
        console.info("已打卡")
        toast("已打卡")
        home()
        sleep(1000)
        return;
    }

    console.log("等待连接到考勤机...")
    sleep(2000)
    
    if (null != textContains("未连接").findOne(1000)) {
        console.error("未连接考勤机, 重新进入考勤界面!")
        back()
        sleep(2000)
        attendKaoqin()
        return;
    }

    textContains("已连接").waitFor()
    console.info("已连接考勤机")
    sleep(1000)

    if (null != textMatches("上班打卡").clickable(true).findOne(1000)) {
        btn_clockin = textMatches("上班打卡").clickable(true).findOnce()
        btn_clockin.click()
        console.log("按下打卡按钮")
    }
    else {
        click(device.width / 2, device.height * 0.560)
        console.log("点击打卡按钮坐标")
    }
    sleep(1000)
    handleLate() // 处理迟到打卡
    
    home()
    sleep(1000)
}


/**
 * @description 下班打卡 
 */
function clockOut() {

    console.log("下班打卡...")
    console.log("等待连接到考勤机...")
    sleep(2000)
    
    if (null != textContains("未连接").findOne(1000)) {
        console.error("未连接考勤机, 重新进入考勤界面!")
        back()
        sleep(2000)
        attendKaoqin()
        return;
    }

    textContains("已连接").waitFor()
    console.info("已连接考勤机")
    sleep(1000)

    if (null != textMatches("下班打卡").clickable(true).findOne(1000)) {
        btn_clockout = textMatches("下班打卡").clickable(true).findOnce()
        btn_clockout.click()
        console.log("按下打卡按钮")
        sleep(1000)
    }
    else {
        click(device.width / 2, device.height * 0.560)
        console.log("点击打卡按钮坐标")
    }

    if (null != textContains("早退打卡").clickable(true).findOne(1000)) {
        className("android.widget.Button").text("早退打卡").clickable(true).findOnce().parent().click()
        console.warn("早退打卡")
    }
    
    home()
    sleep(1000)
}


/**
 * @description 锁屏
 */
function lockScreen(){

    console.log("关闭屏幕")

    // 锁屏方案1:Root
    // Power()

    // 锁屏方案2:No Root
    // press(Math.floor(device.width / 2), Math.floor(device.height * 0.973), 1000) // 小米的快捷手势:长按Home键锁屏
    
    // 万能锁屏方案:向Tasker发送广播, 触发系统锁屏动作。配置方法见 2021-03-09 更新日志
    app.sendBroadcast({action: ACTION_LOCK_SCREEN});

    device.setBrightnessMode(1) // 自动亮度模式
    device.cancelKeepingAwake() // 取消设备常亮
    
    if (isDeviceLocked()) {
        console.info("屏幕已关闭")
    }
    else {
        console.error("屏幕未关闭, 请尝试其他锁屏方案, 或等待屏幕自动关闭")
    }
}



// ===================== ↓↓↓ 功能函数 ↓↓↓ =======================

function dateDigitToString(num){
    return num < 10 ? '0' + num : num
}

function getCurrentTime(){
    var currentDate = new Date()
    var hours = dateDigitToString(currentDate.getHours())
    var minute = dateDigitToString(currentDate.getMinutes())
    var second = dateDigitToString(currentDate.getSeconds())
    var formattedTimeString = hours + ':' + minute + ':' + second
    return formattedTimeString
}

function getCurrentDate(){
    var currentDate = new Date()
    var year = dateDigitToString(currentDate.getFullYear())
    var month = dateDigitToString(currentDate.getMonth() + 1)
    var date = dateDigitToString(currentDate.getDate())
    var week = currentDate.getDay()
    var formattedDateString = year + '-' + month + '-' + date + '-' + WEEK_DAY[week]
    return formattedDateString
}

// 通知过滤器
function filterNotification(bundleId, abstract, text) {
    var check = PACKAGE_ID_WHITE_LIST.some(function(item) {return bundleId == item}) 
    if (!NOTIFICATIONS_FILTER || check) {
        console.verbose(bundleId)
        console.verbose(abstract)
        console.verbose(text)
        console.verbose("---------------------------")
        return true
    }
    else {
        return false 
    }
}

// 保存本地数据
function setStorageData(name, key, value) {
    const storage = storages.create(name)  // 创建storage对象
    storage.put(key, value)
}

// 读取本地数据
function getStorageData(name, key) {
    const storage = storages.create(name)
    if (storage.contains(key)) {
        return storage.get(key, "")
    }
    // 默认返回undefined
}

// 删除本地数据
function delStorageData(name, key) {
    const storage = storages.create(name)
    if (storage.contains(key)) {
        storage.remove(key)
    }
}

// 获取应用版本号
function getPackageVersion(bundleId) {
    importPackage(android.content)
    var pckMan = context.getPackageManager()
    var packageInfo = pckMan.getPackageInfo(bundleId, 0)
    return packageInfo.versionName
}

// 屏幕是否为锁定状态
function isDeviceLocked() {
    importClass(android.app.KeyguardManager)
    importClass(android.content.Context)
    var km = context.getSystemService(Context.KEYGUARD_SERVICE)
    return km.isKeyguardLocked()
}

// 设置媒体和通知音量
function setVolume(volume) {
    device.setMusicVolume(volume)
    device.setNotificationVolume(volume)
    console.verbose("媒体音量:" + device.getMusicVolume())
    console.verbose("通知音量:" + device.getNotificationVolume())
}

📐 工具介绍

Auto.js

Auto.js 是利用安卓系统的 「无障碍服务」 实现类似于按键精灵一样,可以通过代码模拟一系列界面动作的辅助工具。

与 「按键精灵」 不同的是,它的模拟动作并不是简单的使用在界面定坐标点来实现,而是找控件来实现的。

免费版:Auto.js 4.1.1a Alpha2-armeabi-v7a-release

github:GitHub - hyb1996/Auto.js

官方文档:首页 - Auto.js

推荐使用VS Code 插件进行调试,调试完成后,还能通过此插件将脚本保存到手机上。

Tasker

Tasker 也是一个安卓自动化神器,与 Auto.js 结合使用可胜任日常工作流。

此处仅提供 Tasker 5.0 及以下的官方原版,原版不含正版验证,使用不受限制:

Tasker.4.9u4m.apk

Tasker.5.0u7m.apk

Tasker 定时打卡配置:

  1. 添加一个 「通知」 操作任务,通知标题修改为 「定时打卡」,通知文字随意,通知优先级设为 1。
  2. 添加两个配置文件,使用日期和时间作为条件,分别在上班前和下班后触发。

你也可以下载配置文件,导入到 Tasker 中使用,方法如下:

  1. 长按 菜单栏-任务,导入"发送通知.tsk.xml"。
  2. 长按 菜单栏-配置文件,导入"上班打卡.prf.xml" 和 "下班打卡.prf.xml"。
  3. 在任务编辑界面左下方有一个三角形的播放按钮,点击即可发送通知,方便调试。

🕹️ 使用方法

远程打卡

  • 向本机的 QQ 发送消息 「打卡」,或回复标题为 「打卡」 的邮件,或向 PushDeer 发送标题为「打卡」 的推送请求,即可触发打卡进程。
  • 向本机的 QQ 发送消息 「查询」,或回复标题为 「查询」 的邮件,或向 PushDeer 发送标题为「查询」 的推送请求,即可查询最新一次打卡结果。

暂停/恢复定时打卡

  • 向本机的 QQ 发送消息 「暂停」,或回复标题为 「暂停」 的邮件,或向 PushDeer 发送标题为「暂停」 的推送请求,即可暂停定时打卡功能(仅暂停定时打卡,不影响远程打卡功能)
  • 向本机的 QQ 发送消息 「恢复」,或回复标题为 「恢复」 的邮件,或向 PushDeer 发送标题为「恢复」 的推送请求,即可恢复定时打卡功能。

⚠️ 注意事项 (必读!!!)

  • AutoJs Pro 版本屏蔽了一些主流应用,如果要使用 QQ 作为回复方式,不要使用 AutoJs Pro 版!
  • 首次启动 AutoJs,需要为其开启无障碍权限。
  • 运行脚本前,请在 AutoJs 菜单栏中(从屏幕左边划出),开启 「通知读取权限」。
  • 若无法通过 app.launchPackage() 方法启动应用,请开启该应用的「自启动」「允许后台弹窗」。
  • AutoJs、Tasker 可息屏运行,需要在系统设置中开启通知亮屏。
  • 为保证 AutoJs、Tasker 进程不被系统清理,可调整它们的电池管理策略、加入管理应用的白名单,为其开启前台服务、添加应用锁...
  • 虽然脚本可执行完整的打卡步骤,但推荐开启钉钉的极速打卡功能,在钉钉启动时即可完成打卡,应把后续的步骤视为极速打卡失败后的保险措施。

📜 更新日志

2022-03-26

  1. 可以通过 PushDeer 接收通知、推送考勤结果

2022-03-01

  1. 可以通过Server酱来推送考勤结果

2021-10-23

  1. 适配网易邮箱大师7.0

2021-09-02

  1. 新增获取日志功能,发送 「日志」,可将运行日志作为邮件附件发送(最好使用内置邮件)
  2. 优化通知过滤器,过滤 Tasker 发出的无效通知

2021-07-07

  1. 登录流程自动同意隐私协议

2021-05-27

  1. 修改了部分常量的命名
  2. 移除了休息日不打卡的判断
  3. 在邮件的基础上,增加QQ作为新的通讯方式。除发送考勤结果需要手动指定应用外,使用QQ向本机发送 「查询、暂停、恢复」 指令,则会用QQ来回复查询或操作结果;使用邮件向本机发送指令则用邮件回复。

2021-05-06

  1. 增加音量上键监听,按下后中断所有子线程,也可以利用回调来进行调试
  2. 不再使用考勤机名称来判断连接状态
  3. 重新进入打卡界面前,先返回上级菜单,以解决顶号登录无法正常连接到考勤机的问题
  4. 启动钉钉时,将媒体音量和通知音量设为0

2021-03-15

  1. 运行时检查Auto.js版本,脚本需要在Auto.js 4.1.0及以上版本中运行
  2. 新增解锁是否成功的判断,若解锁失败则停止运行脚本
  3. 优化 signIn() 方法,使用 bundleId + activity 来判断登录情况
  4. 优化部分控件和信息的获取方式

2021-03-09

  1. 移除 「结束钉钉」、「检查更新」 这个两个过程,使用最近一次监测到的正在运行的应用的包名进行判断

  2. 补充一个万能锁屏方案:向Tasker发送广播,触发Tasker中的系统锁屏操作。

    • 在Tasker中添加一个任务,在任务中添加操作 「系统锁屏(关闭屏幕)」
    • 在Tasker中添加一个事件类型的配置文件,事件类别:系统-收到的意图
    • 在事件操作中填写:autojs.intent.action.LOCK_SCREEN ,保持发送方与接收方的action一致即可
app.sendBroadcast({
        action: 'autojs.intent.action.LOCK_SCREEN'
    });

2021-02-07

  1. 防止监听事件被耗时操作阻塞。

2021-01-15

  1. 移除 「进入工作台」 以及 「进入考勤打卡界面」 这两个过程
  2. 启动并成功登录钉钉后,直接使用intent拉起考勤打卡界面

2021-01-08

  1. 修复:通知过滤器报错

2020-12-30

  1. 优化:现在可以通过邮件来 暂停/恢复 定时打卡功能,以应对停工停产,或其他需要暂时停止定时打卡的特殊情况

2020-12-04

  1. 优化:打卡过程在子线程中执行,钉钉返回打卡结果后,直接中断子线程,减少无效操作

2020-10-27

  1. 修复:当钉钉的通知文本为null时,indexOf()方法无法正常执行

2020-09-24

  1. 优化:使用URL Scheme直接拉起考勤打卡界面
function attendKaoqin(){
    var a = app.intent({
        action: "VIEW",
        data: "dingtalk://dingtalkclient/page/link?url=https://attend.dingtalk.com/attend/index.html"
      });
      app.startActivity(a);
      sleep(5000)
}

获取URL的方式如下:

  1. 在PC端找到 「智能工作助理」 联系人
  2. 发送消息 “打卡” ,点击 「立即打卡」
  3. 弹出一个二维码。此二维码就是拉起考勤打卡界面的 URL,用自带的相机或其他应用扫描,并在浏览器中打开,即可获得完整URL
  4. 观察获取到的URL,找到 CorpId=xxxxxxxxxxxxxxxxxxx ,将CorpId的值填写到的脚本开头的CORP_ID这个常量中
  5. 仅使用 dingtalk://dingtalkclient/page/link?url=https://attend.dingtalk.com/attend/index.html,也可以拉起旧版打卡界面,钉钉会自动获取企业的CorpId。如果加入了多个组织,且没有填写CorpId,则在拉起考勤界面时会弹出一个选择组织的对话框。

2020-09-11

  1. 将上次考勤结果储存在本地
  2. 将运行日志储存在本地 /sdcard/脚本/Archive/
  3. 修复在下班极速打卡之后,重复打卡的问题

2020-09-04

  1. 将 "打卡" 与 "发送邮件" 分离成两个过程,打卡完成后,将钉钉返回的考勤结果作为邮件正文发送

2020-09-02

  1. 改为使用 "去打卡" 文本获取按钮。若找不到 "去打卡" 按钮,则直接点击 "考勤打卡" 的屏幕坐标

📢 声明

此仓库及脚本仅供学习交流,欢迎转载。旨在让人们关注996制度的存在和非法性,并尝试改变这种现象。

根据1994年第八届全国人大常委会通过和2018年第十三届全国人大常委会修正的《中华人民共和国劳动法》规定,劳动者每日工作时间不超过8小时,平均每周工作时间不超过44小时,而996工作制每周至少要工作72个小时,远超法律标准,因此996工作制度违反劳动法。

而钉钉却允许企业管理者违反法律,非法排班!

第三十六条 国家实行劳动者每日工作时间不超过八小时、平均每周工作时间不超过四十四小时的工时制度。

第四十一条 用人单位由于生产经营需要,经与工会和劳动者协商后可以延长工作时间,一般每日不得超过一小时;因特殊原因需要延长工作时间的,在保障劳动者身体健康的条件下延长工作时间每日不得超过三小时,但是每月不得超过三十六小时。

第四十四条 有下列情形之一的,用人单位应当按照下列标准支付高于劳动者正常工作时间工资的工资报酬:

(一)安排劳动者延长工作时间的,支付不低于工资的百分之一百五十的工资报酬;
(二)休息日安排劳动者工作又不能安排补休的,支付不低于工资的百分之二百的工资报酬;
(三)法定休假日安排劳动者工作的,支付不低于工资的百分之三百的工资报酬。

第九十条 用人单位违反本法规定,延长劳动者工作时间的,由劳动行政部门给予警告,责令改正,并可以处以罚款。

第九十一条 用人单位有下列侵害劳动者合法权益情形之一的,由劳动行政部门责令支付劳动者的工资报酬、经济补偿,并可以责令支付赔偿金:

(二)拒不支付劳动者延长工作时间工资报酬的;

相关项目:996 薪资计算助手


如果觉得还不错的话,就点击右上角, 给我个Star ⭐️ 鼓励一下我吧~

dingding-automatic-clock-in's People

Contributors

georgehuan1994 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

dingding-automatic-clock-in's Issues

请问启动的时候出现这个错误是什么原因呀,魅族手机

18:00:49.516/E: Wrapped java.lang.NullPointerException: Attempt to read from field 'int android.accessibilityservice.AccessibilityServiceInfo.flags' on a null object reference (/storage/emulated/0/脚本/DingDing-Automatic-Clock-in.js#80)
Wrapped java.lang.NullPointerException: Attempt to read from field 'int android.accessibilityservice.AccessibilityServiceInfo.flags' on a null object reference
at /storage/emulated/0/脚本/DingDing-Automatic-Clock-in.js:80:

第80行是这个代码
events.observeKey()

问下用thread能唤醒屏幕吗?

问下用thread能唤醒屏幕吗?

var hour1 = 08, min1 = 20, second1 = 30;
run();
//实时显示脚本运行时长,并对返回的时长与设定的时长进行判断
function run() { //声明运行函数
    threads.shutDownAll();
    threads.start(function () {
        while (true) {
console.show();
console.log(new Date().getSeconds());
            if ((new Date().getHours() * 1 == hour1 && new Date().getMinutes() * 1 == min1 ) {
             
                if (!device.isScreenOn()) {
                    device.wakeUp();
                    sleep(1000);
                    swipe(500, 0, 500, 1900, 2000);
                 gotoDaKa();
                 back();
                    sleep(2000);
                    run();
                }
                else {
                   gotoDaKa();
                    back();
                    sleep(2000);
                    run();
                }
            }
            sleep(1000);
        }
    });
}

ps:在不息屏的情况下执行没问题,但是在息屏threads好像没执行了,问下怎么解决?

关于Wi-Fi打卡问题

没有Wi-Fi考勤机,直接连实验室Wi-Fi打卡,需要修改哪些参数呢?没接触过autojs,望不吝赐教。

多组织不能使用

楼主,测试了一下,如果加入了多组织,会停止在多组织悬浮选择框处,要手动选择

点击打卡按钮无效,是不是和手机屏幕分辨率有关系?

请问下,我发现在执行click(device.width / 2, device.height * 0.560) 的时候感觉程序没点到那个打卡的按钮。怀疑是不是因为分辨率的关系实际点的是旁边的空白地方,不是打卡的按钮上。我应该如何得到这个打卡按钮的实际坐标并且写入程序中呢? 类似这种click(1440,900)?

关于 几处我做了些修改

function signIn() {// 4.进入钉钉
var lgAc = false
var Lg = true
app.launchPackage(BUNDLE_ID_DD)
console.log("启动" + app.getAppName(BUNDLE_ID_DD) + "...")
//sleep(2*1000) // 等待钉钉启动
while(currentPackage()!=BUNDLE_ID_DD){
sleep(1000)
console.log("未启动APP重试")
app.launchPackage(BUNDLE_ID_DD)
}
while(Lg){
console.log(currentActivity())
switch (currentActivity())
{
case "android.widget.FrameLayout":
if (id("home_bottom_tab_text_highlight").exists())
{
console.log("已进入")
lgAc=true
Lg=false
} else {
console.log("启动APP等待中")
lgAc=false
}
break;
case "com.alibaba.android.user.login.SignUpWithPwdActivity":
console.info("账号未登录")
var account = id("et_phone_input").findOne()
account.setText(ACCOUNT)
console.log("输入账号")
var password = id("et_pwd_login").findOne()
password.setText(PASSWORD)
console.log("输入密码")
var btn_login = id("btn_next").findOne()
btn_login.click()
console.log("正在登陆...")
//com.alibaba.android.dingtalkbase.widgets.dialog.DDProgressDialog
break;
}
console.log(currentActivity())
sleep(1000)
if (lgAc==true) break
}
}
1、因为 进入 因机器 不同有的启动慢,有的启动快,所以 这里加的个循环控制
2、是进入 考勤 有些定位 快,有些定位慢,所以 也加了个循环
function InKaoQin(){// 6.进入考勤页
var url_scheme = "dingtalk://dingtalkclient/page/link?url=https://attend.dingtalk.com/attend/index.html"
if(CORP_ID != "") { url_scheme = url_scheme + "?corpId=" + CORP_ID }
var a = app.intent({
action: "VIEW",
data: url_scheme,
//flags: [Intent.FLAG_ACTIVITY_NEW_TASK]
});
app.startActivity(a);
console.log("正在进入考勤界面...")
while (null == textStartsWith("已进入考勤").findOne(1000)){
console.info("GPS")
sleep(1000)
}
console.info("GPS--OK")
if (null != textMatches("申请").clickable(true).findOne(3000)) {
console.info("已进入考勤界面")
}else{
console.log("未找到考勤界面...")
}
}

auto.js运行脚本报错,安卓10

更改了对应的配置信息如的dingding账号密码等之类的,测试运行一下DingDing-Automatic-Clock-in.js 却发现报错了。麻烦看一下是啥原因?


[/storage/emulated/0/脚本/DingDing-Automatic-Clock-in.js]运行结束,用时0.070000秒
20:44:30.083/V: 开始运行[/storage/emulated/0/脚本/DingDing-Automatic-Clock-in.js]
20:44:30.130/D: { major: 4, minor: 1, revision: 0, buildType: 100, build: 1 } { major: 4, minor: 1, revision: 1, buildType: 0, build: 2 }
20:44:30.142/E: Wrapped java.lang.NullPointerException: Attempt to read from field 'int android.accessibilityservice.AccessibilityServiceInfo.flags' on a null object reference (/storage/emulated/0/脚本/DingDing-Automatic-Clock-in.js#72)
Wrapped java.lang.NullPointerException: Attempt to read from field 'int android.accessibilityservice.AccessibilityServiceInfo.flags' on a null object reference
at /storage/emulated/0/脚本/DingDing-Automatic-Clock-in.js:72:0

20:44:30.143/V:

关于一直卡登陆界面的问题

有时候没登录一直在死循环signIn这个函数,是因为et_pwd_login这个获取不到吗,但是钉钉界面确实在登录页,很奇怪。顺便想问下有什么工具可以查看组件id名?我搞前端的,对安卓不是太懂。。。

打卡页面需要刷新

小米手机MIUI13,早上打卡之后还停留在打卡页面,下午在进行打卡时打卡页面提示,请刷新页面后再打卡,往往需要杀掉进程,重新进入才行。
非必现问题。
我尝试在打卡后退一步回到钉钉消息页,既打卡后back(),在监听钉钉返回的考勤结果增加setTimeout,有时可以生效。

  if (packageId == PACKAGE_ID_DD && text.indexOf("考勤打卡") >= 0) {
    setTimeout(function () {
      setStorageData("dingding", "clockResult", text)
      threads.shutDownAll();
      threads.start(function () {
        switch (DEFAULT_MESSAGE_DELIVER) {
          case PUSH_METHOD.QQ:
            sendQQMsg(text);
            break;
          case PUSH_METHOD.Email:
            sendEmail("考勤结果", text, cameraFilePath);
            break;
          case PUSH_METHOD.ServerChan:
            sendServerChan("考勤结果", text);
            break;
          case PUSH_METHOD.PushDeer:
            sendPushDeer("考勤结果", text);
            break;
        }
      });
    }, 5000);
    return;
  }

外勤,备注

你好,请问外勤打卡怎么搞,加上填写备注。
刚接触,不懂怎么找到这两个按钮。

请教一下,该怎么用呢?

最近公司换了钉钉,我想着来这找找有没有解决之道,就发现了这个。但应该怎么用呢?.js文件好像无法下载,我复制了raw,在auto.js里新建了文件然后粘贴进去,运行报错。有点迷茫啊,纯粹的新手小白,希望您能给个教程或简单说一下应该怎么做

小米无法启动钉钉

当界面处于autojs时运行代码可以正常打开应用,而当手机处于home界面则无法启动,请教一下大佬,这种情况该怎么下手去解决

如何运行 auto js 的 调用 邮件发送列表时 获取该 弹窗及其里面的元素 ?

如题 ,你好,现在这个邮件发送的问题一直困扰我,

即在这段代码
app.sendEmail({
email: [EMAILL_ADDRESS],
subject: "考勤结果",
text: message
})

后 弹窗了 邮件发送应用列表 窗体, 我该如何 获取访窗口及其里面的应用 呢 ?

我想用 FloatyWindow.{id} 的方式获取 ‘网易邮箱大师’ 然后模拟点击,

图片

现在问题是这个窗口句柄 及 窗口上的 应用名称 id 该 如何 获取 ,请赐教, 谢谢。

发送邮件时提示未获取权限

把程序停留在应用选择界面,自己打开网易邮箱大师。
也是没权限上传不了附件

但是如果选择其他应用,比如钉钉又是可以把文件发送出去的。

而且从文件管理器手动分享文件,用网易邮箱大师也能发出去。

好怪

Thread[ScriptThread-1[[remote]DingDing-Automatic-Clock-in.js] (Spawn-6),5]: ReferenceError: "PACKAGE_NAME_DD" is not defined. ([remote]DingDing-Automatic-Clock-in.js#352)

[Extension Host] 01:25:40.094/D: 唤醒设备
workbench.desktop.main.js:62 [Extension Host] 01:25:40.106/I: 设备已唤醒
workbench.desktop.main.js:62 [Extension Host] 01:25:40.114/I: 设备已唤醒
workbench.desktop.main.js:62 [Extension Host] 01:25:42.115/D: 解锁屏幕
workbench.desktop.main.js:62 [Extension Host] 01:25:42.120/D: 解锁屏幕
workbench.desktop.main.js:62 [Extension Host] 01:25:44.169/I: 屏幕已解锁
workbench.desktop.main.js:62 [Extension Host] 01:25:44.198/E: Thread[ScriptThread-1[[remote]DingDing-Automatic-Clock-in.js] (Spawn-6),5]: ReferenceError: "PACKAGE_NAME_DD" is not defined. ([remote]DingDing-Automatic-Clock-in.js#352)ReferenceError: "PACKAGE_NAME_DD" is not defined. at signIn ([remote]DingDing-Automatic-Clock-in.js:352:0) at doClock ([remote]DingDing-Automatic-Clock-in.js:196:0) at [remote]DingDing-Automatic-Clock-in.js:123:0
workbench.desktop.main.js:62 [Extension Host] 01:25:44.493/I: 屏幕已解锁
workbench.desktop.main.js:62 [Extension Host] 01:25:44.501/E: Thread[ScriptThread-3[[remote]DingDing-Automatic-Clock-in.js] (Spawn-4),5]: ReferenceError: "PACKAGE_NAME_DD" is not defined. ([remote]DingDing-Automatic-Clock-in.js#352)ReferenceError: "PACKAGE_NAME_DD" is not defined. at signIn ([remote]DingDing-Automatic-Clock-in.js:352:0) at doClock ([remote]DingDing-Automatic-Clock-in.js:196:0) at [remote]DingDing-Automatic-Clock-in.js:123:0

这是啥原因··
小米5 MIUI10

无法填充QQ消息

版本

安卓版本:7.1
QQ版本:8.8.55

现象

自动跳转到指定QQ联系人对话框就没有了,没有在输入框输入内容并点击发送

代码

function sendQQMsg(message) {

    console.log("发送QQ消息")

    // brightScreen()      // 唤醒屏幕
    // unlockScreen()      // 解锁屏幕

    app.startActivity({
        action: "android.intent.action.VIEW",
        data: "mqq://im/chat?chat_type=wpa&version=1&src_type=web&uin=" + QQ,
        packageName: "com.tencent.mobileqq",
    });

    waitForActivity("com.tencent.mobileqq.activity.SplashActivity")
    // input("输入") // 不生效
    console.log(setText("输入")); // 返回false,找不到输入框
    id("input").findOne().setText(message);
    id("fun_btn").findOne().click();
    // home()
    // sleep(1000)
    // lockScreen()    // 关闭屏幕
}

FLAG_ACTIVITY_NEW_TASK

Wrapped android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
我在运行事件监听的时候遇到这样一个错误,怎么处理?

进入考勤页面 未成功打卡

完全不懂技术的新人求问
根据tasker设定的定时提醒
设备定时唤醒
随机时间后启动钉钉
一段时间后打开考勤页面
之后再考勤页面 没有点击上班/下班打卡
之后屏幕关闭

看日记显示有操作下班打卡 但实际未点击下班
这个地方是缺少什么操作吗
还是需要调试页面打卡位置坐标(看页面说明是根据窗口句柄)

WechatIMG103

关于脚本执行停留在:连接钉钉考勤机

我使用qq发送通知后,auto.js能够获取通知并且执行脚本,但是在打开钉钉后就已经自动打卡,无需连接考勤机,所以会导致执行到打卡界面后程序就不再有任何动作了,我等待几分钟后无果,查看脚本日志发现也只执行到了连接考勤机那一步,后面一直是“nul”,请问这个该怎么做,虽然我通过发送打卡后等待两分钟后在发送暂停,在等待之后发送查询也能够获取到打卡信息,但是这脚本的后续仍然没有执行,也没有自动发送消息,请问这种情况我应该怎么做?

是否需要常亮屏幕

这个是否需要常亮屏幕?还是可以关闭屏幕,唤醒时自己解锁屏幕打卡

如何启动?

麻烦问一下, 我将代码的配置参数修改了一下, 就直接将代码放入到autojs中跑了一下, 结果报错了
image

关于远程打卡

图片

图片

我理解的原理 是, tasker 定期发 特定文本的 通知 , 由auto.js 监听捕获特定通知 , 然后执行JS脚本进行打卡,

非常好的思路, 我想问下远程打卡, 发邮件是指发送到绑定 网易邮箱大师的邮件吗, 发送邮件后似乎不能发出相应的 打卡通知 ?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.