added Heroku examples with database store

This commit is contained in:
Wizou 2022-03-28 12:41:28 +02:00
parent 5c5b8032b9
commit 74f22a2f5c
2 changed files with 157 additions and 11 deletions

147
Examples/Program_Heroku.cs Normal file
View file

@ -0,0 +1,147 @@
using Npgsql;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using TL;
// This is an example userbot designed to run on a free Heroku account with a free PostgreSQL database for session storage
// This userbot simply answer "Pong" when someone sends him a "Ping" private message (or in Saved Messages)
// To use/install/deploy this userbot, follow the steps at the end of this file
// When run locally, close the window or type ALT-F4 to exit cleanly and save session (similar to Heroku SIGTERM)
namespace WTelegramClientTest
{
static class Program_Heroku
{
static WTelegram.Client Client;
static User My;
static readonly Dictionary<long, User> Users = new();
static readonly Dictionary<long, ChatBase> Chats = new();
// See steps at the end of this file to setup required Environment variables
static async Task Main(string[] _)
{
var exit = new SemaphoreSlim(0);
AppDomain.CurrentDomain.ProcessExit += (s, e) => exit.Release(); // detect SIGTERM to exit gracefully
var store = new PostgreStore(); // if DB does not contain a session, client will be run in interactive mode
Client = new WTelegram.Client(store.Length == 0 ? null : Environment.GetEnvironmentVariable, store);
using (Client)
{
Client.Update += Client_Update;
My = await Client.LoginUserIfNeeded();
Console.WriteLine($"We are logged-in as {My.username ?? My.first_name + " " + My.last_name} (id {My.id})");
var dialogs = await Client.Messages_GetAllDialogs();
dialogs.CollectUsersChats(Users, Chats);
await exit.WaitAsync();
}
}
private static async void Client_Update(IObject arg)
{
if (arg is not UpdatesBase updates) return;
updates.CollectUsersChats(Users, Chats);
foreach (var update in updates.UpdateList)
{
Console.WriteLine(update.GetType().Name);
if (update is UpdateNewMessage { message: Message { peer_id: PeerUser { user_id: var user_id } } msg }) // private message
if (Users.TryGetValue(user_id, out var user))
{
Console.WriteLine($"New message from {user}: {msg.message}");
if (msg.message.Equals("Ping", StringComparison.OrdinalIgnoreCase))
await Client.SendMessageAsync(user, "Pong");
}
}
}
}
#region PostgreSQL session store
class PostgreStore : Stream
{
private readonly NpgsqlConnection _sql;
private byte[] _data;
private int _dataLen;
private DateTime _lastWrite;
private Task _delayedWrite;
public PostgreStore()
{
var parts = Environment.GetEnvironmentVariable("DATABASE_URL").Split(':', '/', '@'); // parse Heroku DB URL
_sql = new NpgsqlConnection($"User ID={parts[3]};Password={parts[4]};Host={parts[5]};Port={parts[6]};Database={parts[7]};Pooling=true;SSL Mode=Require;Trust Server Certificate=True;");
_sql.Open();
using (var create = new NpgsqlCommand($"CREATE TABLE IF NOT EXISTS WTelegram (name text NOT NULL PRIMARY KEY, data bytea)", _sql))
create.ExecuteNonQuery();
using var cmd = new NpgsqlCommand($"SELECT data FROM WTelegram WHERE name = 'session'", _sql);
using var rdr = cmd.ExecuteReader();
if (rdr.Read())
_dataLen = (_data = rdr[0] as byte[]).Length;
}
protected override void Dispose(bool disposing)
{
_delayedWrite?.Wait();
_sql.Dispose();
}
public override int Read(byte[] buffer, int offset, int count)
{
Array.Copy(_data, 0, buffer, offset, count);
return count;
}
public override void Write(byte[] buffer, int offset, int count) // Write call and buffer modifications are done within a lock()
{
_data = buffer; _dataLen = count;
if (_delayedWrite != null) return;
var left = 1000 - (int)(DateTime.UtcNow - _lastWrite).TotalMilliseconds;
if (left < 0)
{
using var cmd = new NpgsqlCommand($"INSERT INTO WTelegram (name, data) VALUES ('session', @data) ON CONFLICT (name) DO UPDATE SET data = EXCLUDED.data", _sql);
cmd.Parameters.AddWithValue("data", count == buffer.Length ? buffer : buffer[offset..(offset + count)]);
cmd.ExecuteNonQuery();
_lastWrite = DateTime.UtcNow;
}
else // delay writings for a full second
_delayedWrite = Task.Delay(left).ContinueWith(t => { lock (this) { _delayedWrite = null; Write(_data, 0, _dataLen); } });
}
public override long Length => _dataLen;
public override long Position { get => 0; set { } }
public override bool CanSeek => false;
public override bool CanRead => true;
public override bool CanWrite => true;
public override long Seek(long offset, SeekOrigin origin) => 0;
public override void SetLength(long value) { }
public override void Flush() { }
}
#endregion
}
/******************************************************************************************************************************
HOW TO USE AND DEPLOY THIS EXAMPLE HEROKU USERBOT:
- From your free Heroku.com account dashboard, create a new app (Free)
- Navigate to the app Resources and add the add-on "Heroku Postgres" (Hobby Dev - Free)
- Navigate to the app Settings, click Reveal Config Vars and save the Heroku git URL and the value of DATABASE_URL
- Add a new var named "api_hash" with your api hash obtained from https://my.telegram.org/apps
- Add a new var named "phone_number" with the phone_number of the user this userbot will manage
- Scroll down to Buildpacks and add this URL: https://github.com/jincod/dotnetcore-buildpack.git
- In Visual Studio, Clone the Heroku git repository and add some standard .gitignore .gitattributes files
- In this repository folder, create a new .NET Console project with this Program.cs file
- Add these Nuget packages: WTelegramClient and Npgsql
- In Project properties > Debug > Environment variables, configure the same values for DATABASE_URL, api_hash, phone_number
- Run the project in Visual Studio. The first time, it should ask you interactively for elements to complete the connection
- On the following runs, the PostgreSQL database contains the session data and it should connect automatically
- You can test the userbot by sending him "Ping" in private message (or saved messages). It should respond with "Pong"
- You can now commit & push your git sources to Heroku, they will be compiled and deployed/run as a web app
- The userbot should work fine for 1 minute, but it will be taken down because it is not a web app. Let's fix that
- Navigate to the app Resources, copy the line starting with: web cd $HOME/.......
- Back in Visual Studio (or Explorer), at the root of the repository create a Procfile text file (without extension)
- Paste inside the line you copied, and replace the initial "web" with "worker:" (don't forget the colon)
- Commit and push the Git changes to Heroku. Wait until the deployment is complete.
- Verify that the Resources "web" line has changed to "worker" and is enabled (use the pencil icon if necessary)
- Now your userbot should be running 24/7. Note however that a full month of usage is 31*24 = 744 dyno hours.
By default a free account gets 550 free dyno hours per month after which your app is stopped. If you register
a credit card with your account, 450 additional free dyno hours are offered at no charge, which should be enough for 24/7
DISCLAIMER: I'm not affiliated nor expert with Heroku, so if you have any problem with the above I might not be able to help
******************************************************************************************************************************/

View file

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using TL;
@ -10,6 +9,8 @@ namespace WTelegramClientTest
{
static WTelegram.Client Client;
static User My;
static readonly Dictionary<long, User> Users = new();
static readonly Dictionary<long, ChatBase> Chats = new();
// go to Project Properties > Debug > Environment variables and add at least these: api_id, api_hash, phone_number
static async Task Main(string[] _)
@ -21,27 +22,20 @@ namespace WTelegramClientTest
{
Client.Update += Client_Update;
My = await Client.LoginUserIfNeeded();
_users[My.id] = My;
Users[My.id] = My;
// Note that on login Telegram may sends a bunch of updates/messages that happened in the past and were not acknowledged
Console.WriteLine($"We are logged-in as {My.username ?? My.first_name + " " + My.last_name} (id {My.id})");
// We collect all infos about the users/chats so that updates can be printed with their names
var dialogs = await Client.Messages_GetAllDialogs(); // dialogs = groups/channels/users
dialogs.CollectUsersChats(_users, _chats);
dialogs.CollectUsersChats(Users, Chats);
Console.ReadKey();
}
}
private static readonly Dictionary<long, User> _users = new();
private static readonly Dictionary<long, ChatBase> _chats = new();
private static string User(long id) => _users.TryGetValue(id, out var user) ? user.ToString() : $"User {id}";
private static string Chat(long id) => _chats.TryGetValue(id, out var chat) ? chat.ToString() : $"Chat {id}";
private static string Peer(Peer peer) => peer is null ? null : peer is PeerUser user ? User(user.user_id)
: peer is PeerChat or PeerChannel ? Chat(peer.ID) : $"Peer {peer.ID}";
private static void Client_Update(IObject arg)
{
if (arg is not UpdatesBase updates) return;
updates.CollectUsersChats(_users, _chats);
updates.CollectUsersChats(Users, Chats);
foreach (var update in updates.UpdateList)
switch (update)
{
@ -69,5 +63,10 @@ namespace WTelegramClientTest
case MessageService ms: Console.WriteLine($"{Peer(ms.from_id)} in {Peer(ms.peer_id)} [{ms.action.GetType().Name[13..]}]"); break;
}
}
private static string User(long id) => Users.TryGetValue(id, out var user) ? user.ToString() : $"User {id}";
private static string Chat(long id) => Chats.TryGetValue(id, out var chat) ? chat.ToString() : $"Chat {id}";
private static string Peer(Peer peer) => peer is null ? null : peer is PeerUser user ? User(user.user_id)
: peer is PeerChat or PeerChannel ? Chat(peer.ID) : $"Peer {peer.ID}";
}
}