Beispiel #1
0
 public Client(string accessToken)
 {
     webSocket        = new ClientWebSocket();
     MessageReceived += Client_MessageReceived;
     slackCore        = new SlackCore(accessToken);
     slackUsers       = new List <SlackUser>();
     slackChannels    = new List <SlackChannel>();
     outputToChannel  = true;
     debug            = true;
 }
Beispiel #2
0
        private async Task CheckForVenmoDeposits()
        {
            while (true)
            {
                foreach (var workspace in Settings.SettingsObject.Workspaces.Workspaces)
                {
                    WorkspaceInfo workspaceInfo = workspace.Value.ToObject <WorkspaceInfo>() !;
                    logger.LogDebug($"Processing workspace ${workspace.Key}");
                    MongoDatabase database   = new MongoDatabase(workspace.Key, mongoDatabaseLogger);
                    SlackCore     slackApi   = new SlackCore(workspaceInfo.BotToken);
                    var           slackUsers = await slackApi.UsersList();

                    List <Database.Models.VenmoUser> users = database.GetAllUsers();
                    foreach (var user in users)
                    {
                        if (user.YNAB != null && user.YNAB.DefaultAccount != null)
                        {
                            // Get YNAB access token
                            Configuration config          = helperMethods.CreateConfiguration(user.YNAB.Auth !.AccessToken !);
                            string?       ynabAccessToken = await helperMethods.CheckIfYNABAccessTokenIsExpired(user, database, new ApiClient(config));

                            if (ynabAccessToken == null)
                            {
                                logger.LogError($"Unable to refresh YNAB access token for {user.UserId}");
                                await WebhookController.SendSlackMessage(workspaceInfo, "Unable to import your Venmo deposits into YNAB because your YNAB authentication information is invalid. Please refresh it.",
                                                                         user.UserId, httpClient);

                                continue;
                            }
                            config = helperMethods.CreateConfiguration(ynabAccessToken);

                            // Get Venmo access token
                            VenmoApi venmoApi         = new VenmoApi(venmoApiLogger);
                            string?  venmoAccessToken = await helperMethods.CheckIfVenmoAccessTokenIsExpired(user, venmoApi, database);

                            if (string.IsNullOrEmpty(venmoAccessToken))
                            {
                                logger.LogError($"Unable to refresh Venmo access token for {user.UserId}");
                                await WebhookController.SendSlackMessage(workspaceInfo,
                                                                         "Unable to process scheduled Venmos as your token has expired. Please refresh it.",
                                                                         user.UserId, httpClient);

                                continue;
                            }
                            venmoApi.AccessToken = venmoAccessToken;

                            logger.LogDebug($"Processing slack user {user.UserId}");

                            List <TransactionDetail> venmoDeposits = await GetVenmoDeposits(user, config);

                            if (!ShouldProcessLatestVenmoDeposit(venmoDeposits))
                            {
                                continue;
                            }

                            // (fromDate - toDate]. We can adjust which transactions get included after if the
                            // subtransactions total doesn't match the deposit total
                            DateOnly toDate = DateOnly.FromDateTime(venmoDeposits[0].Date !.Value);
                            DateOnly fromDate;
                            // if multiple deposits were made on the same day (due to Venmo transfer limits) then
                            // make sure we get the first deposit not on the same day
                            foreach (var deposit in venmoDeposits)
                            {
                                fromDate = DateOnly.FromDateTime(deposit.Date !.Value);
                                if (fromDate != toDate)
                                {
                                    break;
                                }
                            }
                            fromDate = fromDate.AddDays(1);

                            var venmoTransactions = await GetVenmoTransactionsBetweenDates(fromDate, toDate, venmoApi, user);

                            TransactionDetail         ynabDeposit         = venmoDeposits[0];
                            SaveTransaction?          newTransaction      = null;
                            List <SaveSubTransaction> subTransactions     = new List <SaveSubTransaction>();
                            VenmoTransaction?         previousTransaction = null;
                            decimal originalDepositAmount = 0;
                            decimal depositAmount         = 0;
                            bool    multipleTransfers     = false;
                            foreach (VenmoTransaction transaction in venmoTransactions)
                            {
                                if (previousTransaction == null)
                                {
                                    depositAmount         = transaction.Transfer !.Amount;
                                    originalDepositAmount = depositAmount;
                                }
                                else
                                {
                                    if (previousTransaction.Type == "transfer" && transaction.Type == "transfer")
                                    {
                                        multipleTransfers     = true;
                                        depositAmount        += transaction.Transfer !.Amount;
                                        originalDepositAmount = depositAmount;
                                    }

                                    if (transaction.Type == "payment")
                                    {
                                        if (previousTransaction.Type == "transfer" && multipleTransfers)
                                        {
                                            long   milliunitAmount = (long)(depositAmount * 1000m);
                                            string importId        = $"YNAB:{milliunitAmount}:{ynabDeposit.Date:yyyy-MM-dd}";
                                            newTransaction = new SaveTransaction(ynabDeposit.AccountId, ynabDeposit.Date,
                                                                                 amount: milliunitAmount, ynabDeposit.PayeeId, ynabDeposit.PayeeName,
                                                                                 ynabDeposit.CategoryId, memo: null, golf1052.YNABAPI.HelperMethods.ClearedEnum.Uncleared,
                                                                                 ynabDeposit.Approved, ynabDeposit.FlagColor, importId: importId,
                                                                                 subTransactions);
                                        }

                                        if (depositAmount - transaction.Payment !.Amount > 0)
                                        {
                                            string?memo       = transaction.Payment.Note;
                                            Guid?  categoryId = null;
                                            if (user.YNAB.Mapping != null && user.YNAB.Mapping.Count > 0 && memo != null)
                                            {
                                                foreach (var mapping in user.YNAB.Mapping)
                                                {
                                                    if (mapping.VenmoNote.Equals(memo, StringComparison.InvariantCultureIgnoreCase))
                                                    {
                                                        categoryId = new Guid(mapping.CategoryId);
                                                        break;
                                                    }
                                                }
                                            }
                                            var subTransaction = new SaveSubTransaction(amount: (long)(transaction.Payment.Amount * 1000),
                                                                                        payeeId: null, payeeName: null, categoryId: categoryId, memo);
                                            subTransactions.Add(subTransaction);
                                            depositAmount -= transaction.Payment.Amount;
                                        }
                                    }
                                }
                                previousTransaction = transaction;
                            }

                            if (depositAmount > 0)
                            {
                                var unknownSubTransaction = new SaveSubTransaction(amount: (long)(depositAmount * 1000),
                                                                                   payeeId: null, payeeName: null, categoryId: null, memo: "UNKNOWN");
                                subTransactions.Add(unknownSubTransaction);
                            }

                            TransactionsApi transactionsApi = new TransactionsApi(config);
                            if (multipleTransfers)
                            {
                                // we created a new transaction so POST it
                                var createTransactionsResponse = await transactionsApi.CreateTransactionAsync(Default,
                                                                                                              new SaveTransactionsWrapper(newTransaction));

                                // then "delete" the other transfers
                                List <UpdateTransaction> transactionsToDelete = new List <UpdateTransaction>();
                                for (int i = 0; i < venmoDeposits.Count; i++)
                                {
                                    TransactionDetail transactionDetail = venmoDeposits[i];
                                    VenmoTransaction  venmoTransaction  = venmoTransactions[i];
                                    if (venmoTransaction.Type != "transfer")
                                    {
                                        break;
                                    }

                                    UpdateTransaction updateTransaction = new UpdateTransaction(transactionDetail.AccountId,
                                                                                                transactionDetail.Date, transactionDetail.Amount, transactionDetail.PayeeId,
                                                                                                transactionDetail.PayeeName, transactionDetail.CategoryId,
                                                                                                memo: $"<DELETE ME: Combined into ${originalDepositAmount:F2} Transfer from Venmo> {transactionDetail.Memo}",
                                                                                                golf1052.YNABAPI.HelperMethods.ClearedEnum.Uncleared,
                                                                                                transactionDetail.Approved, transactionDetail.FlagColor, transactionDetail.ImportId,
                                                                                                new List <SaveSubTransaction>());
                                    transactionsToDelete.Add(updateTransaction);
                                }

                                var updateTransactionsResponse = await transactionsApi.UpdateTransactionsAsync(Default, new UpdateTransactionsWrapper(transactionsToDelete));

                                await WebhookController.SendSlackMessage(workspaceInfo,
                                                                         $"Imported and created a new YNAB Venmo transaction totaling ${originalDepositAmount:F2} with " +
                                                                         $"{subTransactions.Count} Venmo subtransactions combining {transactionsToDelete.Count} " +
                                                                         $"transactions which occured on {newTransaction!.Date:d}. " +
                                                                         $"Please go into YNAB, confirm the new transaction and delete the old transactions.",
                                                                         user.UserId, httpClient);
                            }
                            else
                            {
                                // we didn't create a new transaction so just PUT the existing transaction
                                newTransaction = new SaveTransaction(ynabDeposit.AccountId, ynabDeposit.Date,
                                                                     ynabDeposit.Amount, ynabDeposit.PayeeId, ynabDeposit.PayeeName, ynabDeposit.CategoryId,
                                                                     ynabDeposit.Memo, golf1052.YNABAPI.HelperMethods.ClearedEnum.Uncleared, ynabDeposit.Approved,
                                                                     ynabDeposit.FlagColor, ynabDeposit.ImportId, subTransactions);
                                var response = await transactionsApi.UpdateTransactionAsync(Default, ynabDeposit.Id,
                                                                                            new SaveTransactionWrapper(newTransaction));

                                await WebhookController.SendSlackMessage(workspaceInfo,
                                                                         $"Updated a YNAB Venmo transaction with {subTransactions.Count} Venmo subtransactions which occured" +
                                                                         $"on {newTransaction.Date:d}. Please go into YNAB and confirm the updated transaction.",
                                                                         user.UserId, httpClient);
                            }
                        }
                    }
                }
                await Task.Delay(CheckDuration.ToTimeSpan());
            }
        }
Beispiel #3
0
        public async Task <string> HandleWebhook([FromBody] JObject data)
        {
            logger.LogInformation(data.ToString(Formatting.None));
            VenmoWebhookRequest request = data.ToObject <VenmoWebhookRequest>() !;
            string message = string.Empty;

            if (request.Type == "payment.created")
            {
                var databaseInfo = GetUserFromDatabases(request.Data.Target.User.Id);
                if (databaseInfo == null)
                {
                    return("");
                }

                if (WebhookSeenBefore(databaseInfo.Value.user, request.Data.Id, request.Data.Status))
                {
                    return("");
                }

                SaveWebhookId(databaseInfo.Value.user, request.Data.Id, request.Data.Status, databaseInfo.Value.database);
                SlackCore slackApi = new SlackCore(databaseInfo.Value.workspaceInfo.BotToken);
                message += $"{request.Data.Actor.DisplayName} ({request.Data.Actor.Username}) ";

                if (request.Data.Action == "pay")
                {
                    message += "paid you ";
                }
                else if (request.Data.Action == "charge")
                {
                    message += "charged you ";
                }
                message += $"${request.Data.Amount:F2} for {request.Data.Note}";

                if (request.Data.Action == "pay")
                {
                    await SendSlackMessage(databaseInfo.Value.workspaceInfo, message, databaseInfo.Value.user.UserId, httpClient);
                }

                if (request.Data.Action == "charge")
                {
                    message += $" | ID: {request.Data.Id}";
                    string        acceptCommand = $"venmo complete accept {request.Data.Id}";
                    List <IBlock> blocks        = new List <IBlock>();
                    blocks.Add(new Section(TextObject.CreatePlainTextObject($"{message}\n/{acceptCommand}"), null, null,
                                           new golf1052.SlackAPI.BlockKit.BlockElements.Image(request.Data.Actor.ProfilePictureUrl,
                                                                                              $"Venmo profile picture of {request.Data.Actor.DisplayName}")));
                    blocks.Add(new Actions(
                                   new Button("Accept", "acceptButton", null, acceptCommand, null, null),
                                   new Button("Reject", "rejectButton", null, $"venmo complete reject {request.Data.Id}", null, null)));

                    var channels = await slackApi.ConversationsList(false, "im");

                    var userChannel = channels.FirstOrDefault(c => c.User == databaseInfo.Value.user.UserId);
                    if (userChannel != null)
                    {
                        await SendSlackMessage(databaseInfo.Value.workspaceInfo, $"{message}\n/{acceptCommand}", blocks, userChannel.Id, httpClient);
                    }
                }
            }
            else if (request.Type == "payment.updated")
            {
                if (request.Data.Target.Type != "user")
                {
                    return("");
                }

                // When user charges someone else and their payment is completed the user is the actor.
                // When user is charged by someone else and their payment is completed the user is the target.
                var databaseInfo = GetUserFromDatabases(request.Data.Actor.Id);
                if (databaseInfo == null)
                {
                    return("");
                }

                if (WebhookSeenBefore(databaseInfo.Value.user, request.Data.Id, request.Data.Status))
                {
                    return("");
                }
                SaveWebhookId(databaseInfo.Value.user, request.Data.Id, request.Data.Status, databaseInfo.Value.database);
                message += $"{request.Data.Target.User.DisplayName} ({request.Data.Target.User.Username}) ";

                if (request.Data.Status == "settled")
                {
                    message += "accepted your ";
                }
                else if (request.Data.Status == "cancelled")
                {
                    message += "rejected your ";
                }
                message += $"${request.Data.Amount:F2} charge for {request.Data.Note}";
                await SendSlackMessage(databaseInfo.Value.workspaceInfo, message, databaseInfo.Value.user.UserId, httpClient);
            }
            return("");
        }
        private async Task CheckSchedules()
        {
            while (true)
            {
                foreach (var workspace in Settings.SettingsObject.Workspaces.Workspaces)
                {
                    WorkspaceInfo workspaceInfo = workspace.Value.ToObject <WorkspaceInfo>() !;
                    MongoDatabase database      = new MongoDatabase(workspace.Key, mongoDatabaseLogger);
                    SlackCore     slackApi      = new SlackCore(workspaceInfo.BotToken);
                    var           slackUsers    = await slackApi.UsersList();

                    List <Database.Models.VenmoUser> users = database.GetAllUsers();
                    foreach (var user in users)
                    {
                        if (user.Schedule != null && user.Schedule.Count > 0)
                        {
                            VenmoApi venmoApi    = new VenmoApi(venmoApiLogger);
                            string?  accessToken = await helperMethods.CheckIfVenmoAccessTokenIsExpired(user, venmoApi, database);

                            if (string.IsNullOrEmpty(accessToken))
                            {
                                logger.LogError($"Unable to refresh Venmo access token for {user.UserId}");
                                await WebhookController.SendSlackMessage(workspaceInfo,
                                                                         "Unable to process scheduled Venmos as your token has expired. Please refresh it.",
                                                                         user.UserId, httpClient);

                                continue;
                            }

                            venmoApi.AccessToken = accessToken;
                            bool saveNeeded = false;
                            for (int i = 0; i < user.Schedule.Count; i++)
                            {
                                VenmoSchedule schedule       = user.Schedule[i];
                                Instant       now            = clock.GetCurrentInstant();
                                Instant       scheduledTime  = Instant.FromDateTimeUtc(schedule.NextExecution);
                                bool          deleteSchedule = false;
                                if (now > scheduledTime)
                                {
                                    saveNeeded = true;
                                    await WebhookController.SendSlackMessage(workspaceInfo,
                                                                             $"Processing {schedule.Command}",
                                                                             user.UserId,
                                                                             httpClient);

                                    string[] splitPaymentMessage = helperMethods.ConvertScheduleMessageIntoPaymentMessage(schedule.Command.Split(' '));

                                    ParsedVenmoPayment parsedVenmoPayment;
                                    try
                                    {
                                        parsedVenmoPayment = helperMethods.ParseVenmoPaymentMessage(splitPaymentMessage);
                                    }
                                    catch (Exception ex)
                                    {
                                        logger.LogWarning(ex, "Failed to parse payment, this shouldn't happen as it was parsed before being saved.");
                                        continue;
                                    }

                                    var response = await helperMethods.VenmoPayment(venmoApi, user, database,
                                                                                    parsedVenmoPayment.Amount, parsedVenmoPayment.Note, parsedVenmoPayment.Recipients,
                                                                                    parsedVenmoPayment.Action, parsedVenmoPayment.Audience);

                                    foreach (var r in response.responses)
                                    {
                                        if (!string.IsNullOrEmpty(r.Error))
                                        {
                                            await WebhookController.SendSlackMessage(workspaceInfo,
                                                                                     $"Venmo error: {r.Error}",
                                                                                     user.UserId, httpClient);

                                            continue;
                                        }

                                        if (parsedVenmoPayment.Action == VenmoAction.Charge)
                                        {
                                            await WebhookController.SendSlackMessage(workspaceInfo,
                                                                                     $"Successfully charged {r.Data!.Payment.Target!.User.Username} ${r.Data.Payment.Amount} for {r.Data.Payment.Note}. Audience is {r.Data.Payment.Audience}",
                                                                                     user.UserId, httpClient);
                                        }
                                        else
                                        {
                                            await WebhookController.SendSlackMessage(workspaceInfo,
                                                                                     $"Successfully paid {r.Data!.Payment.Target!.User.Username} ${r.Data.Payment.Amount} for {r.Data.Payment.Note}. Audience is {r.Data.Payment.Audience}",
                                                                                     user.UserId, httpClient);
                                        }
                                    }

                                    foreach (var u in response.unprocessedRecipients)
                                    {
                                        await WebhookController.SendSlackMessage(workspaceInfo,
                                                                                 $"You are not friends with {u}.",
                                                                                 user.UserId, httpClient);
                                    }

                                    if (schedule.Verb == "at" || schedule.Verb == "on")
                                    {
                                        deleteSchedule = true;
                                    }
                                    else if (schedule.Verb == "every")
                                    {
                                        SlackUser?slackUser = helperMethods.GetSlackUser(user.UserId, slackUsers);
                                        if (slackUser == null)
                                        {
                                            // user somehow doesn't exist?
                                            logger.LogError($"While trying to process schedule for a slack user they disappeared? {user.UserId}");
                                            deleteSchedule = true;
                                        }
                                        else
                                        {
                                            ZonedDateTime nextExecution = helperMethods.ConvertScheduleMessageIntoDateTime(
                                                schedule.Command.Split(' '),
                                                slackUser.TimeZone,
                                                clock);
                                            await WebhookController.SendSlackMessage(workspaceInfo,
                                                                                     $"Next execution is {nextExecution.GetFriendlyZonedDateTimeString()}",
                                                                                     user.UserId,
                                                                                     httpClient);

                                            schedule.NextExecution = nextExecution.ToDateTimeUtc();
                                        }
                                    }
                                }

                                if (deleteSchedule)
                                {
                                    user.Schedule.RemoveAt(i);
                                    i--;
                                }
                            }

                            if (saveNeeded)
                            {
                                database.SaveUser(user);
                            }
                        }
                    }
                }

                await Task.Delay(CheckDuration.ToTimeSpan());
            }
        }