Welcome to lark3ri.com

Financial management program with c++

28.11.2020

Financial management program which I program for more detailed financial management. Includes only the features I actually need so I can keep it simple to manage my finances. Includes enough features, as example, working command and command line parameters are:

./program.exe database-path=data start-date="15.11.2020 02:00:00" end-date="15.11.2020 02:00:03" credit-first no-color delete=1 it deletes the first result between those dates.

I use Cygwin to run this program on Windows. I have not tested functionality on any other platform. Default database path is data. You can find more default settings to be set in std::map<std::string, std::string> sett, line 57. Here is a picture of the program:


main.cpp
/*
	Copyright(C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110 - 1301, USA.

	Version: 1.2
*/

#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <iomanip>

#ifdef _WIN32
#include <windows.h>
#pragma execution_character_set("utf-8")
#endif

//#define _DEBUG



class SETT
{
	const std::string settings_fp = "settings";
	const std::string translation_dp = "translations/";

	bool veccmp(const std::vector<std::string> &arr, const std::string &val)
	{
		for (std::vector<std::string>::const_iterator it = arr.begin(); it != arr.end(); it++)
		{
			if (it->compare(val) == 0)
			{
				return true;
			}
		}
		return false;
	}

	std::vector<std::string> cmd_arg = {
		"SV_NEW",
		"SV_DELETE"
	};

	std::map<std::string, std::string> sett = {
		{"SV_DATABASE_PATH",    "data"},
		{"SV_LANGUAGE",         "en"},
		{"SV_TIMEZONE",         "+0"},
		{"SV_DATE_ORDER",       "DMYhms"},
		{"SV_CREDIT_COLOR",     "1;32"},
		{"SV_DEBIT_COLOR",      "1;35"},
		{"SV_ORDER_BY_DATE",    "0"},
		{"SV_DEBIT_FIRST",      "0"},
		{"SV_CREDIT_FIRST",     "0"},
		{"SV_ONLY_DEBIT",       "0"},
		{"SV_ONLY_CREDIT",      "0"},
		{"SV_NO_COLOR",         "0"},
		{"SV_NEW",              "0"},
		{"SV_DELETE",           ""},
		{"SV_START_DATE",       ""},
		{"SV_END_DATE",         ""},
		{"SV_FORMAT",           "%d.%m.%Y %H:%M:%S"},
		{"SV_ORDER",            "cod-dat-nam-amo-ref-des-tzo"},
		{"SV_CURRENCY",         "$"},
		{"SV_CURRENCY_SIDE",    "1"},

		{"T_CREDIT_OR_DEBIT",   "Credit or Debit?(credit/debit): "},
		{"T_CREDIT_OR_DEBIT_A", "credit"},
		{"T_DAY",               "Day:\t"},
		{"T_MONTH",             "Month:\t"},
		{"T_YEAR",              "Year:\t"},
		{"T_HOURS",             "Hours:\t"},
		{"T_MINUTES",           "Minutes:\t"},
		{"T_SECONDS",           "Seconds:\t"},
		{"T_NAME",              "Name:\t\t"},
		{"T_AMOUNT",            "Amount:\t\t"},
		{"T_REF",               "Ref:\t\t"},
		{"T_DESCRIPTION",       "Description:\t"},
		{"T_TIMEZONE",          "Timezone:\t"},
		{"T_DATE",              "Date:\t\t"},
		{"T_BALANCE",           "Balance: "},
		{"T_CREDIT",            "CREDIT"},
		{"T_DEBIT",             "DEBIT"}
	};

	public:
		SETT();
		std::string get(const std::string &key);
		bool getb(const std::string& key);
		bool add(std::string key, const std::string &val);
};

SETT::SETT()
{
	// Read settings file
	std::ifstream file(settings_fp);
	if (!file.good())
	{
		return;
	}
	if (file.peek() == std::ifstream::traits_type::eof())
	{
		return;
	}

	std::string current_line;
	while (std::getline(file, current_line))
	{
		if (current_line.empty()) continue;

		std::string key;
		std::string val;

		int index = current_line.find("=");
		if (index != -1)
		{
			key = current_line.substr(0, index);
			val = current_line.substr(index + 1, current_line.size() - 1);
		}
		else
		{
			key = current_line;
			val = "1";
		}

		for (unsigned int i = 0; i < key.size(); i++)
		{
			char c = key.at(i);
			// abc --> ABC
			if (c > 96 && c < 123)
			{
				c -= 32;
				key.at(i) = c;
			}
		}

		if (!veccmp(cmd_arg, key) && key.at(0) == 'S' && key.at(1) == 'V')
		{
			std::map<std::string, std::string>::iterator it = sett.find(key);
			if (it != sett.end())
			{
				it->second = val;
			}
		}
	}

	// Read translations file
	std::string langauge_fn = get("SV_LANGUAGE");
	if (!langauge_fn.empty())
	{
		std::ifstream file0(translation_dp + langauge_fn);
		if (!file0.good())
		{
			return;
		}
		if (file0.peek() == std::ifstream::traits_type::eof())
		{
			return;
		}

		while (std::getline(file0, current_line))
		{
			if (current_line.empty()) continue;

			std::string key;
			std::string val;

			int index = current_line.find("=");
			if (index != -1)
			{
				key = current_line.substr(0, index);
				val = current_line.substr(index + 1, current_line.size() - 1);
			}
			else
			{
				key = current_line;
				val = "1";
			}

			for (unsigned int i = 0; i < key.size(); i++)
			{
				char c = key.at(i);
				// abc --> ABC
				if (c > 96 && c < 123)
				{
					c -= 32;
					key.at(i) = c;
				}
			}

			if (!veccmp(cmd_arg, key) && key.at(0) == 'T')
			{
				std::map<std::string, std::string>::iterator it = sett.find(key);
				if (it != sett.end())
				{
					it->second = val;
				}
			}
		}
	}

#ifdef _DEBUG
	for (std::map<std::string, std::string>::iterator it = sett.begin(); it != sett.end(); it++)
	{
		std::cout << it->first << "=" << it->second << std::endl;
	}
#endif
}

std::string SETT::get(const std::string &key)
{
	std::map<std::string, std::string>::iterator it = sett.find(key);
	if (it != sett.end())
	{
		return it->second;
	}
	return std::string("");
}

bool SETT::getb(const std::string& key)
{
	std::map<std::string, std::string>::iterator it = sett.find(key);
	if (it != sett.end())
	{
		return std::stoi(it->second) > 0;
	}
	return false;
}

bool SETT::add(std::string key, const std::string &val)
{
	for (unsigned int i = 0; i < key.size(); i++)
	{
		char c = key.at(i);
		if (c == '-')
		{
			key.at(i) = '_';
			continue;
		}
		// abc --> ABC
		if (c > 96 && c < 123)
		{
			c -= 32;
			key.at(i) = c;
		}
	}
	key = std::string("SV_") + key;
	std::map<std::string, std::string>::iterator it = sett.find(key);
	if (it != sett.end())
	{
		it->second = val;
		return false;
	}
	return true;
}



struct transaction {
	time_t date;
	time_t date_current;
	std::string reference_num;
	int timezone;
	bool credit_or_debit;
	double amount;
	std::string name;
	std::string description;
	unsigned int index;
};
std::vector<transaction> data; // All data.
std::vector<transaction> data0; // Data visible.

void read_database(const std::string &fn);
bool save_database(const std::string &fn);
int set_timezone(const std::string &tz);
std::string get_timezone(int timezone);

time_t date_to_seconds(const std::string& date)
{
	int yy, month, dd, hh, mm, ss;
	const char* datec = date.c_str();
	sscanf(datec, "%d.%d.%d %d:%d:%d", &dd, &month, &yy, &hh, &mm, &ss);
	
	struct tm tm;
	tm.tm_year = yy - 1900;
	tm.tm_mon = month - 1;
	tm.tm_mday = dd;
	tm.tm_hour = hh;
	tm.tm_min = mm;
	tm.tm_sec = ss;
	tm.tm_isdst = -1;

	time_t t = mktime(&tm);
	return t;
}

time_t date_to_seconds(const std::string& date, std::string format)
{
	char u[6] = {0};
	int i = 0;
	for (std::string::iterator it = format.begin(); it != format.end(); it++)
	{
		if (*it == '%')
		{
			u[i] = *(it + 1);
			*(it + 1) = 'd';
			i++;
		}
	}

	int y[6] = {0};
	const char* datec = date.c_str();
	sscanf(datec, format.c_str(), &y[0], &y[1], &y[2], &y[3], &y[4], &y[5]);

	int yy, month, dd, hh, mm, ss;
	for (i = 0; i < 6; i++)
	{
		if (u[i] == 'd')
		{
			dd = y[i];
			continue;
		}
		if (u[i] == 'm')
		{
			month = y[i];
			continue;
		}
		if (u[i] == 'Y')
		{
			yy = y[i];
			continue;
		}
		if (u[i] == 'H')
		{
			hh = y[i];
			continue;
		}
		if (u[i] == 'M')
		{
			mm = y[i];
			continue;
		}
		if (u[i] == 'S')
		{
			ss = y[i];
		}
	}

	struct tm tm;
	tm.tm_year = yy - 1900;
	tm.tm_mon = month - 1;
	tm.tm_mday = dd;
	tm.tm_hour = hh;
	tm.tm_min = mm;
	tm.tm_sec = ss;
	tm.tm_isdst = -1;

	time_t t = mktime(&tm);
	return t;
}

void get_word(std::string *val)
{
	std::string usr_input;
	std::getline(std::cin, usr_input);
	if (usr_input.empty())
	{
		*val = "";
		return;
	}
	int find_pos = usr_input.find(" ");
	if (find_pos != -1)
	{
		usr_input = usr_input.substr(0, find_pos);
	}
	*val = usr_input;
}



int main(int argc, char *argv[])
{
#ifdef _WIN32
	SetConsoleOutputCP(65001);
#endif

	std::string fp;
	std::vector<std::string> cmd_arg;
	SETT sett;

	if (argc > 1)
	{
		cmd_arg = std::vector<std::string>(argv, argv + argc);
		cmd_arg.erase(cmd_arg.begin());
		cmd_arg.shrink_to_fit();

		for (std::vector<std::string>::iterator it = cmd_arg.begin(); it != cmd_arg.end(); it++)
		{
			std::string lhs;
			std::string rhs;
			int index = it->find('=');
			if (index != -1)
			{
				lhs = it->substr(0, index);
				rhs = it->substr(index + 1, it->size());
			}
			else
			{
				static bool prevent_double = false;
				if (!prevent_double)
				{
					prevent_double = true;
					std::vector<std::string>::iterator it0 = it;
					for (; it0 != cmd_arg.end(); it0++)
					{
						int index0 = it0->find('=');
						if (index0 != -1)
							return 0;
					}
				}
				lhs = *it;
				rhs = "1";
			}
			sett.add(lhs, rhs);
		}
	}

	int timezone = set_timezone(sett.get("SV_TIMEZONE"));
	const std::string format = sett.get("SV_FORMAT");
	const std::string order = sett.get("SV_ORDER");
	
	fp = sett.get("SV_DATABASE_PATH");
	read_database(fp);

	// Order by date
	if (!data.empty())
	{
		for (unsigned int i = 0; i < data.size() - 1; i++)
		{
			if (data.at(i).date > data.at(i + 1).date)
			{
				for (unsigned int u = i; u < data.size() - 1; u++)
				{
					if (data.at(u).date < data.at(u + 1).date) break;

					std::vector<transaction>::iterator it = data.begin() + u;
					data.insert(data.begin() + u + 2, *it);
					data.erase(data.begin() + u);
				}
				if (i > 0) i -= 2;
				else i--;
			}
		}
		data0 = data;
	}

	if (argc > 1)
	{
		// SV_NEW - cmd arg
		if (sett.getb("SV_NEW"))
		{
			transaction new_data;
			for (unsigned int i = 0; i < 7; i++)
			{
				std::string usr_input;

				char current[4];
				current[0] = order.at(4 * i);
				current[1] = order.at(4 * i + 1);
				current[2] = order.at(4 * i + 2);
				current[3] = '\0';

				// credit or debit
				if (strcmp(current, "cod") == 0)
				{
					std::cout << sett.get("T_CREDIT_OR_DEBIT");
					get_word(&usr_input);
					if (usr_input.empty())
						return 0;
					if (strcmp(usr_input.c_str(), sett.get("T_CREDIT_OR_DEBIT_A").c_str()) == 0)
					{
						usr_input = "1";
					}
					else
					{
						usr_input = "0";
					}
					new_data.credit_or_debit = std::stoi(usr_input.c_str()) > 0;
				}
				// date
				if (strcmp(current, "dat") == 0)
				{
					std::string dmy = sett.get("SV_DATE_ORDER");
					std::string dd, month, yy, mm, hh, ss;
					for (unsigned int i = 0; i < 6; i++)
					{
						if (dmy.at(i) == 'D')
						{
							std::cout << sett.get("T_DAY");
							get_word(&dd);
							if (dd.empty())
								return 0;
							continue;
						}
						if (dmy.at(i) == 'M')
						{
							std::cout << sett.get("T_MONTH");
							get_word(&month);
							if (month.empty())
								return 0;
							continue;
						}
						if (dmy.at(i) == 'Y')
						{
							std::cout << sett.get("T_YEAR");
							get_word(&yy);
							if (yy.empty())
								return 0;
							continue;
						}
						if (dmy.at(i) == 'h')
						{
							std::cout << sett.get("T_HOURS");
							get_word(&hh);
							if (hh.empty())
								hh = "0";
							continue;
						}
						if (dmy.at(i) == 'm')
						{
							std::cout << sett.get("T_MINUTES");
							get_word(&mm);
							if (mm.empty())
								mm = "0";
							continue;
						}
						if (dmy.at(i) == 's')
						{
							std::cout << sett.get("T_SECONDS");
							get_word(&ss);
							if (ss.empty())
								ss = "0";
							continue;
						}
					}
					std::string date = dd;
					date += ".";
					date += month;
					date += ".";
					date += yy;
					date += " ";
					date += hh;
					date += ":";
					date += mm;
					date += ":";
					date += ss;
					new_data.date = date_to_seconds(date);
				}
				// name
				if (strcmp(current, "nam") == 0)
				{
					std::cout << sett.get("T_NAME");
					get_word(&usr_input);
					if (usr_input.empty())
						usr_input = "-";
					new_data.name = usr_input;
				}
				// amount
				if (strcmp(current, "amo") == 0)
				{
					std::cout << sett.get("T_AMOUNT");
					get_word(&usr_input);
					if (usr_input.empty())
						usr_input = "-";
					new_data.amount = std::atof(usr_input.c_str());
				}
				// reference
				if (strcmp(current, "ref") == 0)
				{
					std::cout << sett.get("T_REF");
					get_word(&usr_input);
					if (usr_input.empty())
						usr_input = "-";
					new_data.reference_num = usr_input;
				}
				// description
				if (strcmp(current, "des") == 0)
				{
					std::cout << sett.get("T_DESCRIPTION");
					get_word(&usr_input);
					if (usr_input.empty())
						usr_input = "-";
					new_data.description = usr_input;
				}
				// timezone
				if (strcmp(current, "tzo") == 0)
				{
					std::cout << sett.get("T_TIMEZONE");
					get_word(&usr_input);
					if (usr_input.empty())
					{
						new_data.timezone = timezone;
					}
					else
					{
						new_data.timezone = set_timezone(usr_input);
					}
					new_data.date -= new_data.timezone;
				}
				std::cin.clear();
				fflush(stdin);
			}
			new_data.date_current = std::time(nullptr);
#ifdef _DEBUG
			std::cout << std::endl;
			std::cout << "----- NEW DATA -----" << std::endl;
			std::cout << "date:            " << new_data.date << std::endl;
			std::cout << "date_current:    " << new_data.date_current << std::endl;
			std::cout << "reference_num:   " << new_data.reference_num << std::endl;
			std::cout << "timezone:        " << new_data.timezone << std::endl;
			std::cout << "credit_or_debit: " << new_data.credit_or_debit << std::endl;
			std::cout << "amount:          " << new_data.amount << std::endl;
			std::cout << "name:            " << new_data.name << std::endl;
			std::cout << "description:     " << new_data.description << std::endl;
			time_t tt = new_data.date_current + timezone;
			std::cout << "ct: " << std::put_time(gmtime(&tt), format.c_str()) << std::endl;
			tt = new_data.date + new_data.timezone;
			std::cout << "dt: " << std::put_time(gmtime(&tt), format.c_str()) << std::endl;
			return 0;
#endif
			data.push_back(new_data);

			save_database(fp);
			return 0;
		}

		if (data0.empty())
		{
			return 0;
		}

		// SV_START_DATE - cmd arg
		std::string start_date = sett.get("SV_START_DATE");
		if (!start_date.empty())
		{
			time_t date = date_to_seconds(start_date, format.c_str());
			for (std::vector<transaction>::iterator it = data0.begin(); it != data0.end(); it++)
			{
				if (it->date + it->timezone < date)
				{
					data0.erase(it);
					it--;
				}
			}
		}
		// SV_END_DATE - cmd arg
		std::string end_date = sett.get("SV_END_DATE");
		if (!end_date.empty())
		{
			time_t date = date_to_seconds(end_date, format.c_str());
			for (std::vector<transaction>::iterator it = data0.begin(); it != data0.end(); it++)
			{
				if (it->date + it->timezone > date)
				{
					data0.erase(it);
					it--;
				}
			}
		}

		// SV_ORDER_BY_DATE - cmd arg
		if (sett.getb("SV_ORDER_BY_DATE"))
		{
			for (unsigned int i = 0; i < data0.size() - 1; i++)
			{
				if (data0.at(i).date_current > data0.at(i + 1).date_current)
				{
					for (unsigned int u = i; u < data0.size() - 1; u++)
					{
						if (data0.at(u).date_current < data0.at(u + 1).date_current) break;

						std::vector<transaction>::iterator it = data0.begin() + u;
						data0.insert(data0.begin() + u + 2, *it);
						data0.erase(data0.begin() + u);
					}
					if (i > 0) i -= 2;
					else i--;
				}
			}
		}

		// SV_DEBIT_FIRST - cmd arg
		if (sett.getb("SV_DEBIT_FIRST"))
		{
			std::vector<transaction> temp;
			for (std::vector<transaction>::iterator it = data0.begin(); it != data0.end(); it++)
			{
				if (it->credit_or_debit == 0)
				{
					transaction tra = *it;
					data0.erase(it);
					temp.push_back(tra);
					it--;
				}
			}
			data0.insert(data0.end(), temp.begin(), temp.end());
		}
		// SV_CREDIT_FIRST - cmd arg
		else if (sett.getb("SV_CREDIT_FIRST"))
		{
			std::vector<transaction> temp;
			for (std::vector<transaction>::iterator it = data0.begin(); it != data0.end(); it++)
			{
				if (it->credit_or_debit == 1)
				{
					transaction tra = *it;
					data0.erase(it);
					temp.push_back(tra);
					it--;
				}
			}
			data0.insert(data0.end(), temp.begin(), temp.end());
		}
		// SV_ONLY_DEBIT - cmd arg
		else if (sett.getb("SV_ONLY_DEBIT"))
		{
			for (std::vector<transaction>::iterator it = data0.begin(); it != data0.end(); it++)
			{
				if (it->credit_or_debit == 1)
				{
					data0.erase(it);
					it--;
				}
			}
		}
		// SV_ONLY_CREDIT - cmd arg
		else if (sett.getb("SV_ONLY_CREDIT"))
		{
			for (std::vector<transaction>::iterator it = data0.begin(); it != data0.end(); it++)
			{
				if (it->credit_or_debit == 0)
				{
					data0.erase(it);
					it--;
				}
			}
		}

		// SV_DELETE - cmd arg
		std::string delete_cmd = sett.get("SV_DELETE");
		if (!delete_cmd.empty())
		{
			if (data0.empty())
			{
				return 0;
			}
			for (std::string::iterator it = delete_cmd.begin(); it != delete_cmd.end(); it++)
			{
				if (*it > 57 || *it < 48)
				{
					return 0;
				}
			}
			unsigned int index = std::stoi(delete_cmd);
			if (index < 1 || index > data0.size())
			{
				return 0;
			}

			transaction tra = data0.at(data0.size() - index);

			std::vector<transaction>::const_iterator it;
			it = data.begin() + tra.index;
			data.erase(it);

			save_database(fp);
			return 0;
		}
	}

	if (data0.empty())
	{
		return 0;
	}

	const std::string currency = sett.get("SV_CURRENCY");
	double balance = 0;
	bool currency_side = sett.getb("SV_CURRENCY_SIDE");

	std::cout << std::fixed << std::setprecision(2);

	unsigned int index = 0;
	for (std::vector<transaction>::const_iterator it = data0.begin(); it != data0.end(); it++)
	{
		std::cout << "\n[ " << data0.size() - index++  << " ]" << std::endl;
		for (unsigned int i = 0; i < 7; i++)
		{
			char current[4];
			current[0] = order.at(4 * i);
			current[1] = order.at(4 * i + 1);
			current[2] = order.at(4 * i + 2);
			current[3] = '\0';

			// timezone
			if (strcmp(current, "tzo") == 0)
			{
				continue;
			}

			// credit or debit
			if (strcmp(current, "cod") == 0)
			{
				bool colored_text = sett.getb("SV_NO_COLOR");
				if (it->credit_or_debit)
				{
					std::string text = sett.get("T_CREDIT");
					if (colored_text)
					{
						std::cout << text << std::endl;
					}
					else
					{
						std::string color = "\033[";
						color += sett.get("SV_CREDIT_COLOR");
						color += "m";
						std::cout << color << text << "\033[0m" << std::endl;
					}
				}
				else
				{
					std::string text = sett.get("T_DEBIT");
					if (colored_text)
					{
						std::cout << text << std::endl;
					}
					else
					{
						std::string color = "\033[";
						color += sett.get("SV_DEBIT_COLOR");
						color += "m";
						std::cout << color << text << "\033[0m" << std::endl;
					}
				}
				continue;
			}
			// date
			if (strcmp(current, "dat") == 0)
			{
				std::cout << sett.get("T_DATE");
				time_t date = it->date + it->timezone;
				std::cout << std::put_time(gmtime(&date), format.c_str()) << std::endl;
				if (it->credit_or_debit)
				{
					balance += it->amount;
				}
				else
				{
					balance -= it->amount;
				}
				continue;
			}
			// name
			if (strcmp(current, "nam") == 0)
			{
				std::cout << sett.get("T_NAME") << it->name << std::endl;
				continue;
			}
			// amount
			if (strcmp(current, "amo") == 0)
			{
				std::cout << sett.get("T_AMOUNT");
				if (currency_side)
				{
					std::cout << currency << it->amount << std::endl;
				}
				else
				{
					std::cout << it->amount << currency << std::endl;
				}
				continue;
			}
			// reference
			if (strcmp(current, "ref") == 0)
			{
				std::cout << sett.get("T_REF");
				std::cout << it->reference_num << std::endl;
				continue;
			}
			// description
			if (strcmp(current, "des") == 0)
			{
				std::cout << sett.get("T_DESCRIPTION") << it->description << std::endl;
			}
		}
	}

	std::cout << "\n" << sett.get("T_BALANCE");
	if (currency_side)
	{
		std::cout << currency << balance << std::endl;
	}
	else
	{
		std::cout << balance << currency << std::endl;
	}

	return 0;
}

void read_database(const std::string &fn)
{
	std::ifstream file(fn);

	if (!file.good())
	{
		return;
	}
	if (file.peek() == std::ifstream::traits_type::eof())
	{
		return;
	}

	std::string current_line;
	unsigned int index = 0;
	while (std::getline(file, current_line))
	{
		data.push_back(transaction());
		data.back().date = std::stoi(current_line.c_str());

		std::getline(file, current_line);
		data.back().date_current = std::stoi(current_line.c_str());

		std::getline(file, current_line);
		data.back().name = current_line;

		std::getline(file, current_line);
		data.back().amount = std::atof(current_line.c_str());

		std::getline(file, current_line);
		data.back().reference_num = current_line;

		std::getline(file, current_line);
		data.back().description = current_line;
		
		std::getline(file, current_line);
		int timezone = set_timezone(current_line);
		data.back().timezone = timezone;

		std::getline(file, current_line);
		data.back().credit_or_debit = std::stoi(current_line.c_str()) > 0;

		data.back().index = index;
		index++;
	}
}

bool save_database(const std::string &fn)
{
	std::ofstream file(fn);
	if (!file.good())
	{
		std::cout << "\n\nERROR! The database can not be saved." << std::endl;
		return true;
	}
	for (std::vector<transaction>::const_iterator it = data.begin(); it != data.end(); it++)
	{
		file << it->date << "\n";
		file << it->date_current << "\n";
		file << it->name << "\n";
		file << it->amount << "\n";
		file << it->reference_num << "\n";
		file << it->description << "\n";
		file << get_timezone(it->timezone) << "\n";
		file << it->credit_or_debit << "\n";
	}
	return false;
}

int set_timezone(const std::string& tz)
{
	std::string sn(1, tz.at(1));
	if (tz.size() > 2)
	{
		std::string c(1, tz.at(2));
		sn += c;
	}
	
	int n = std::stoi(sn.c_str());
	if (n < -12) n = -12;
	else if (n > 12) n = 12;
	
	int timezone;
	timezone = 3600 * n;
	if (tz.at(0) == '-')
	{
		timezone *= -1;
	}
	return timezone;
}

std::string get_timezone(int timezone)
{
	std::string return_str;
	if (timezone < 0)
	{
		timezone *= -1;
		return_str = "-";
	}
	else
	{
		return_str = "+";
	}
	int n = timezone / 3600;
	return_str += std::to_string(n);
	return return_str;
}

Supports also translations into a different languages. Translated into Finnish, the file may look like this for example:

translations/fi
T_DATE=Päivämäärä:	
T_AMOUNT=Summa:		
T_NAME=Nimi:		
T_DESCRIPTION=Kuvaus:		
T_BALANCE=Yhteensä: 
T_REF=Viitenumero:	
T_CREDIT_OR_DEBIT=Tulo vai meno?(tulo/meno): 
T_CREDIT_OR_DEBIT_A=tulo
T_DAY=Päivä: 		
T_MONTH=Kuukausi: 	
T_YEAR=Vuosi: 		
T_MINUTES=Minuutit: 	
T_HOURS=Tunnit: 	
T_SECONDS=Sekunnit: 	
T_TIMEZONE=Aikavyöhyke:	
T_DEBIT=MENO
T_CREDIT=TULO

The Settings file may look like this for example:

settings
SV_LANGUAGE=fi
SV_TIMEZONE=+2
SV_FORMAT=%d.%m.%Y %H:%M:%S
SV_CURRENCY=€
SV_CURRENCY_SIDE=0

Matrix and vector classes with c++

28.10.2020

I wrote these classes a couple of years ago. They became a byproduct of when I developed my own 3d game engine. I never got to finish it, still, not yet. It loaded 3d objects from files and drew those in 3d and it was fun to make. It was made using only SDL2 2d library. There was also a cube in the game. The camera was able to rotate around the cube by moving the mouse. Also the cube could be moved with the WASD keys.

I remember that I have tested the functionality of these classes. I remember some bug was in the matrix class, somewhere on top of it. The bug didn't affect its use in any way so I left it there.

matrix.h
/*
    Copyright (C) 2017 Lari Varjonen <[email protected]>

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    version 2 as published by the Free Software Foundation.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

#ifndef MATRIX_H
#define MATRIX_H

#include <cstring>

#include "vector.h"

/*     A   B   C   D
 * a { 00, 01, 02, 03 }
 * b { 10, 11, 12, 13 }
 * c { 20, 21, 22, 23 }
 * d { 30, 31, 32, 33 }
 *
 * aA, bA, cA = x, y, z = Forward Vector,
 * aB, bB, cB = x, y, z = Up Vector,
 * aC, bC, cC = x, y, z = Right Vector,
 * aD, bD, cD = x, y, z = Translation Vector.
 *
 * Matrix = Matrix +/- Vector
 * Matrix +=/-= Vector
 * Adding or subtracting translation.
 *
 * T *get_row(unsigned int p) const;
 * T *get_column(unsigned int p) const;
 * Return a dynamically allocated array. Remember to delete[] it.
*/

template<class T>
class Matrix
{
public:
    Matrix() {identity();}
    Matrix(T x0,T y0,T z0,
           T x1,T y1,T z1,
           T x2,T y2,T z2,
           T xT,T yT,T zT,
           T w0,T w1,T w2,T w3);
    Matrix(const Vector<T> &forward,
           const Vector<T> &up,
           const Vector<T> &right);
    Matrix(const T (&forward)[3],
           const T (&up)[3],
           const T (&right)[3]);
    Matrix(const T (&array)[4][4]);
    Matrix(const Matrix &_m);

    Matrix<T>& operator =(const T (&rhs)[4][4]);
    Matrix<T>& operator =(const T rhs);
    Matrix<T>& operator =(const Matrix &rhs);

    Matrix<T>& operator *=(const T rhs);
    Matrix<T>& operator *=(const Matrix &rhs);
    Matrix<T> operator *(const T rhs);
    Matrix<T> operator *(const Matrix &rhs);
    Vector<T> operator *(const Vector<T> &rhs);
    Vector<T> transform_vector(const Vector<T> &rhs);

    Matrix<T>& operator +=(const T rhs);
    Matrix<T>& operator +=(const Matrix<T> &rhs);
    Matrix<T>& operator +=(const Vector<T> &rhs);
    Matrix<T> operator +(const T rhs);
    Matrix<T> operator +(const Matrix<T> &rhs);
    Matrix<T> operator +(const Vector<T> &rhs);

    Matrix<T>& operator -=(const T rhs);
    Matrix<T>& operator -=(const Matrix<T> &rhs);
    Matrix<T>& operator -=(const Vector<T> &rhs);
    Matrix<T> operator -(const T rhs);
    Matrix<T> operator -(const Matrix<T> &rhs);
    Matrix<T> operator -(const Vector<T> &rhs);

    bool operator ==(const T (&rhs)[4][4]) const;
    bool operator ==(const T rhs) const;
    bool operator ==(const Matrix &rhs) const;
    bool operator ==(const Vector<T> &rhs) const;
    bool operator !=(const T (&rhs)[4][4]) const;
    bool operator !=(const T rhs) const;
    bool operator !=(const Matrix &rhs) const;
    bool operator !=(const Vector<T> &rhs) const;

    void identity();
    bool is_identity() const;

    Vector<T> get_forward_vector() const;
    Vector<T> get_up_vector() const;
    Vector<T> get_right_vector() const;
    Vector<T> get_translation_vector() const;

    void set_scale(T x,T y,T z);
    void set_scale(const Vector<T> &v);
    void set_rotation(double a, T x, T y, T z);
    void set_rotation(double a, const Vector<T> &v);
    void set_translation(T x,T y,T z);
    void set_translation(const Vector<T> &v);
    void set_forward(T x,T y,T z);
    void set_forward(const Vector<T> &v);
    void set_up(T x,T y,T z);
    void set_up(const Vector<T> &v);
    void set_right(T x,T y,T z);
    void set_right(const Vector<T> &v);

    T *get_row(unsigned int p) const;
    void set_row(unsigned int p,T r0,T r1,T r2,T r3);
    T *get_column(unsigned int p) const;
    void set_column(unsigned int p,T c0,T c1,T c2,T c3);
    void set_column(unsigned int p, const Vector<T> &v,T c3);

    T m[4][4];

    const unsigned int ROW_SIZE = sizeof(m)/sizeof(m[0]);
    const unsigned int COLUMN_SIZE = sizeof(m[0])/sizeof(T);

private:
    template<class U>
    friend std::ostream &operator <<(std::ostream &, const Matrix<U> &);
};

template<class T>
std::ostream& operator <<(std::ostream &os, const Matrix<T> &m)
{
    os << "Matrix\n";
    for (unsigned int r = 0; r < m.ROW_SIZE; r++)
    {
        os << "[";
        for (unsigned int c = 0; c < m.COLUMN_SIZE; c++)
            os << m.m[r][c] << ((c<3)?",":"]\n");
    }
    return os << "\n";
}

template<class T>
Matrix<T>::Matrix(T x0,T y0,T z0,
                  T x1,T y1,T z1,
                  T x2,T y2,T z2,
                  T xT,T yT,T zT,
                  T w0,T w1,T w2,T w3)
{
    m[0][0] = x0; m[0][1] = x1; m[0][2] = x2; m[0][3] = xT;
    m[1][0] = y0; m[1][1] = y1; m[1][2] = y2; m[1][3] = yT;
    m[2][0] = z0; m[2][1] = z1; m[2][2] = z2; m[2][3] = zT;
    m[3][0] = w0; m[3][1] = w1; m[3][2] = w2; m[3][3] = w3;
}

template<class T>
Matrix<T>::Matrix(const Matrix &_m)
{
    memcpy(m, _m.m, sizeof m);
}

template<class T>
Matrix<T>::Matrix(const Vector<T> &f,
               const Vector<T> &u,
               const Vector<T> &r)
{
    *this = {{f.x,u.x,r.x,0},
             {f.y,u.y,r.y,0},
             {f.z,u.z,r.z,0},
             {0,0,0,1}};
}

template<class T>
Matrix<T>::Matrix(const T (&f)[3],
                  const T (&u)[3],
                  const T (&r)[3])
{
    *this = {{f[0],u[0],r[0],0},
             {f[1],u[1],r[1],0},
             {f[2],u[2],r[2],0},
             {0,0,0,1}};
}

template<class T>
Matrix<T>::Matrix(const T (&array)[4][4])
{
    *this = array;
}

// Basic assignment
template<class T>
Matrix<T>& Matrix<T>::operator =(const T (&rhs)[4][4])
{
    if (*this != rhs) memcpy(m, rhs, sizeof m);
    return *this;
}
template<class T>
Matrix<T>& Matrix<T>::operator =(const T rhs)
{
    return *this = {{rhs,rhs,rhs,rhs},
                    {rhs,rhs,rhs,rhs},
                    {rhs,rhs,rhs,rhs},
                    {rhs,rhs,rhs,rhs}};
}
template<class T>
Matrix<T>& Matrix<T>::operator =(const Matrix<T> &rhs)
{
    return *this = rhs.m;
}

// Multiplication
template<class T>
Matrix<T>& Matrix<T>::operator *=(const T rhs)
{
    m[0][0] *= rhs; m[0][1] *= rhs; m[0][2] *= rhs; m[0][3] *= rhs;
    m[1][0] *= rhs; m[1][1] *= rhs; m[1][2] *= rhs; m[1][3] *= rhs;
    m[2][0] *= rhs; m[2][1] *= rhs; m[2][2] *= rhs; m[2][3] *= rhs;
    m[3][0] *= rhs; m[3][1] *= rhs; m[3][2] *= rhs; m[3][3] *= rhs;
    return *this;
}
template<class T>
Matrix<T>& Matrix<T>::operator *=(const Matrix &rhs)
{
    T r[ROW_SIZE][COLUMN_SIZE];
    r[0][0] = m[0][0]*rhs.m[0][0] + m[0][1]*rhs.m[1][0] + m[0][2]*rhs.m[2][0] + m[0][3]*rhs.m[3][0];
    r[0][1] = m[0][0]*rhs.m[0][1] + m[0][1]*rhs.m[1][1] + m[0][2]*rhs.m[2][1] + m[0][3]*rhs.m[3][1];
    r[0][2] = m[0][0]*rhs.m[0][2] + m[0][1]*rhs.m[1][2] + m[0][2]*rhs.m[2][2] + m[0][3]*rhs.m[3][2];
    r[0][3] = m[0][0]*rhs.m[0][3] + m[0][1]*rhs.m[1][3] + m[0][2]*rhs.m[2][3] + m[0][3]*rhs.m[3][3];

    r[1][0] = m[1][0]*rhs.m[0][0] + m[1][1]*rhs.m[1][0] + m[1][2]*rhs.m[2][0] + m[1][3]*rhs.m[3][0];
    r[1][1] = m[1][0]*rhs.m[0][1] + m[1][1]*rhs.m[1][1] + m[1][2]*rhs.m[2][1] + m[1][3]*rhs.m[3][1];
    r[1][2] = m[1][0]*rhs.m[0][2] + m[1][1]*rhs.m[1][2] + m[1][2]*rhs.m[2][2] + m[1][3]*rhs.m[3][2];
    r[1][3] = m[1][0]*rhs.m[0][3] + m[1][1]*rhs.m[1][3] + m[1][2]*rhs.m[2][3] + m[1][3]*rhs.m[3][3];

    r[2][0] = m[2][0]*rhs.m[0][0] + m[2][1]*rhs.m[1][0] + m[2][2]*rhs.m[2][0] + m[2][3]*rhs.m[3][0];
    r[2][1] = m[2][0]*rhs.m[0][1] + m[2][1]*rhs.m[1][1] + m[2][2]*rhs.m[2][1] + m[2][3]*rhs.m[3][1];
    r[2][2] = m[2][0]*rhs.m[0][2] + m[2][1]*rhs.m[1][2] + m[2][2]*rhs.m[2][2] + m[2][3]*rhs.m[3][2];
    r[2][3] = m[2][0]*rhs.m[0][3] + m[2][1]*rhs.m[1][3] + m[2][2]*rhs.m[2][3] + m[2][3]*rhs.m[3][3];

    r[3][0] = m[3][0]*rhs.m[0][0] + m[3][1]*rhs.m[1][0] + m[3][2]*rhs.m[2][0] + m[3][3]*rhs.m[3][0];
    r[3][1] = m[3][0]*rhs.m[0][1] + m[3][1]*rhs.m[1][1] + m[3][2]*rhs.m[2][1] + m[3][3]*rhs.m[3][1];
    r[3][2] = m[3][0]*rhs.m[0][2] + m[3][1]*rhs.m[1][2] + m[3][2]*rhs.m[2][2] + m[3][3]*rhs.m[3][2];
    r[3][3] = m[3][0]*rhs.m[0][3] + m[3][1]*rhs.m[1][3] + m[3][2]*rhs.m[2][3] + m[3][3]*rhs.m[3][3];

    memcpy(m, r, sizeof m);
    return *this;
}
template<class T>
Matrix<T> Matrix<T>::operator *(const T rhs)
{
    return Matrix(*this) *= rhs;
}
template<class T>
Matrix<T> Matrix<T>::operator *(const Matrix &rhs)
{
    return Matrix(*this) *= rhs;
}
template<class T>
Vector<T> Matrix<T>::operator *(const Vector<T> &rhs)
{
    return Vector<T>(m[0][0]*rhs.x + m[0][1]*rhs.y + m[0][2]*rhs.z + m[0][3]*m[0][3],
                     m[1][0]*rhs.x + m[1][1]*rhs.y + m[1][2]*rhs.z + m[1][3]*m[1][3],
                     m[2][0]*rhs.x + m[2][1]*rhs.y + m[2][2]*rhs.z + m[2][3]*m[2][3]);
}
template<class T>
Vector<T> Matrix<T>::transform_vector(const Vector<T> &rhs)
{
    return Vector<T>(m[0][0]*rhs.x + m[0][1]*rhs.y + m[0][2]*rhs.z + m[0][3],
                     m[1][0]*rhs.x + m[1][1]*rhs.y + m[1][2]*rhs.z + m[1][3],
                     m[2][0]*rhs.x + m[2][1]*rhs.y + m[2][2]*rhs.z + m[2][3]);
}

// Addition
template<class T>
Matrix<T>& Matrix<T>::operator +=(const T rhs)
{
    m[0][0] += rhs; m[0][1] += rhs; m[0][2] += rhs; m[0][3] += rhs;
    m[1][0] += rhs; m[1][1] += rhs; m[1][2] += rhs; m[1][3] += rhs;
    m[2][0] += rhs; m[2][1] += rhs; m[2][2] += rhs; m[2][3] += rhs;
    m[3][0] += rhs; m[3][1] += rhs; m[3][2] += rhs; m[3][3] += rhs;
    return *this;
}
template<class T>
Matrix<T>& Matrix<T>::operator +=(const Matrix<T> &rhs)
{
    m[0][0] += rhs.m[0][0]; m[0][1] += rhs.m[0][1]; m[0][2] += rhs.m[0][2]; m[0][3] += rhs.m[0][3];
    m[1][0] += rhs.m[1][0]; m[1][1] += rhs.m[1][1]; m[1][2] += rhs.m[1][2]; m[1][3] += rhs.m[1][3];
    m[2][0] += rhs.m[2][0]; m[2][1] += rhs.m[2][1]; m[2][2] += rhs.m[2][2]; m[2][3] += rhs.m[2][3];
    m[3][0] += rhs.m[3][0]; m[3][1] += rhs.m[3][1]; m[3][2] += rhs.m[3][2]; m[3][3] += rhs.m[3][3];
    return *this;
}
template<class T>
Matrix<T>& Matrix<T>::operator +=(const Vector<T> &rhs)
{
    m[0][3] += rhs.x;
    m[1][3] += rhs.y;
    m[2][3] += rhs.z;
    return *this;
}
template<class T>
Matrix<T> Matrix<T>::operator +(const T rhs)
{
    return Matrix(*this) += rhs;
}
template<class T>
Matrix<T> Matrix<T>::operator +(const Matrix<T> &rhs)
{
    return Matrix(*this) += rhs;
}
template<class T>
Matrix<T> Matrix<T>::operator +(const Vector<T> &rhs)
{
    return Matrix(*this) += rhs;
}

// Subtraction
template<class T>
Matrix<T>& Matrix<T>::operator -=(const T rhs)
{
    return *this += -rhs;
}
template<class T>
Matrix<T>& Matrix<T>::operator -=(const Matrix<T> &rhs)
{
    m[0][0] -= rhs.m[0][0]; m[0][1] -= rhs.m[0][1]; m[0][2] -= rhs.m[0][2]; m[0][3] -= rhs.m[0][3];
    m[1][0] -= rhs.m[1][0]; m[1][1] -= rhs.m[1][1]; m[1][2] -= rhs.m[1][2]; m[1][3] -= rhs.m[1][3];
    m[2][0] -= rhs.m[2][0]; m[2][1] -= rhs.m[2][1]; m[2][2] -= rhs.m[2][2]; m[2][3] -= rhs.m[2][3];
    m[3][0] -= rhs.m[3][0]; m[3][1] -= rhs.m[3][1]; m[3][2] -= rhs.m[3][2]; m[3][3] -= rhs.m[3][3];
    return *this;
}
template<class T>
Matrix<T>& Matrix<T>::operator -=(const Vector<T> &rhs)
{
    m[0][3] -= rhs.x;
    m[1][3] -= rhs.y;
    m[2][3] -= rhs.z;
    return *this;
}
template<class T>
Matrix<T> Matrix<T>::operator -(const T rhs)
{
    return Matrix(*this) -= rhs;
}
template<class T>
Matrix<T> Matrix<T>::operator -(const Matrix<T> &rhs)
{
    return Matrix(*this) -= rhs;
}
template<class T>
Matrix<T> Matrix<T>::operator -(const Vector<T> &rhs)
{
    return Matrix(*this) -= rhs;
}

// Comparison operators
template<class T>
bool Matrix<T>::operator ==(const T (&rhs)[4][4]) const
{
    return m[0][0] == rhs[0][0] && m[0][1] == rhs[0][1] && m[0][2] == rhs[0][2] && m[0][3] == rhs[0][3] &&
           m[1][0] == rhs[1][0] && m[1][1] == rhs[1][1] && m[1][2] == rhs[1][2] && m[1][3] == rhs[1][3] &&
           m[2][0] == rhs[2][0] && m[2][1] == rhs[2][1] && m[2][2] == rhs[2][2] && m[2][3] == rhs[2][3] &&
           m[3][0] == rhs[3][0] && m[3][1] == rhs[3][1] && m[3][2] == rhs[3][2] && m[3][3] == rhs[3][3];
}
template<class T>
bool Matrix<T>::operator ==(const T rhs) const
{
    return this->operator ==({{rhs,rhs,rhs,rhs},
                              {rhs,rhs,rhs,rhs},
                              {rhs,rhs,rhs,rhs},
                              {rhs,rhs,rhs,rhs}});
}
template<class T>
bool Matrix<T>::operator ==(const Matrix &rhs) const
{
    return *this == rhs.m;
}
template<class T>
bool Matrix<T>::operator ==(const Vector<T> &rhs) const
{
    return m[0][3] == rhs.x && m[1][3] == rhs.y && m[2][3] == rhs.z;
}
template<class T>
bool Matrix<T>::operator !=(const T (&rhs)[4][4]) const
{
    return !(*this == rhs);
}
template<class T>
bool Matrix<T>::operator !=(const T rhs) const
{
    return !(*this == rhs);
}
template<class T>
bool Matrix<T>::operator !=(const Matrix &rhs) const
{
    return !(*this == rhs);
}
template<class T>
bool Matrix<T>::operator !=(const Vector<T> &rhs) const
{
    return !(*this == rhs);
}

template<class T>
void Matrix<T>::identity()
{
    memset(m, 0, sizeof m);
    m[0][0] = m[1][1] = m[2][2] = m[3][3] = 1;
}

template<class T>
bool Matrix<T>::is_identity() const
{
    return m[0][0] == 1 && m[0][1] == 0 && m[0][2] == 0 && m[0][3] == 0 &&
           m[1][0] == 0 && m[1][1] == 1 && m[1][2] == 0 && m[1][3] == 0 &&
           m[2][0] == 0 && m[2][1] == 0 && m[2][2] == 1 && m[2][3] == 0 &&
           m[3][0] == 0 && m[3][1] == 0 && m[3][2] == 0 && m[3][3] == 1;
}

// Get vector
template<class T>
Vector<T> Matrix<T>::get_forward_vector() const
{
    return Vector<T>(m[0][0],m[1][0],m[2][0]);
}
template<class T>
Vector<T> Matrix<T>::get_up_vector() const
{
    return Vector<T>(m[0][1],m[1][1],m[2][1]);
}
template<class T>
Vector<T> Matrix<T>::get_right_vector() const
{
    return Vector<T>(m[0][2],m[1][2],m[2][2]);
}
template<class T>
Vector<T> Matrix<T>::get_translation_vector() const
{
    return Vector<T>(m[0][3],m[1][3],m[2][3]);
}

// Set scale
template<class T>
void Matrix<T>::set_scale(T x,T y,T z)
{
    m[0][0] = x;
    m[1][1] = y;
    m[2][2] = z;
}
template<class T>
void Matrix<T>::set_scale(const Vector<T> &v)
{
    set_scale(v.x, v.y, v.z);
}

// Set rotation
template<class T>
void Matrix<T>::set_rotation(double a,T x,T y,T z)
{
    double c = cos(a);
    double s = sin(a);
    double t = 1-c;

    m[0][0] = x*x*t + c;
    m[1][0] = y*x*t + z*s;
    m[2][0] = z*x*t - y*s;

    m[0][1] = x*y*t - z*s;
    m[1][1] = y*y*t + c;
    m[2][1] = z*y*t + x*s;

    m[0][2] = x*z*t + y*s;
    m[1][2] = y*z*t - x*s;
    m[2][2] = z*z*t + c;
}
template<class T>
void Matrix<T>::set_rotation(double a, const Vector<T> &v)
{
    set_rotation(a, v.x, v.y, v.z);
}

// Set translation
template<class T>
void Matrix<T>::set_translation(T x,T y,T z)
{
    m[0][3] = x;
    m[1][3] = y;
    m[2][3] = z;
}
template<class T>
void Matrix<T>::set_translation(const Vector<T> &v)
{
    set_translation(v.x, v.y, v.z);
}

// Set FUR vectors
template<class T>
void Matrix<T>::set_forward(T x,T y,T z)
{
    m[0][0] = x;
    m[1][0] = y;
    m[2][0] = z;
}
template<class T>
void Matrix<T>::set_forward(const Vector<T> &v)
{
    set_forward_vector(v.x, v.y, v.z);
}
template<class T>
void Matrix<T>::set_up(T x,T y,T z)
{
    m[0][1] = x;
    m[1][1] = y;
    m[2][1] = z;
}
template<class T>
void Matrix<T>::set_up(const Vector<T> &v)
{
    set_up_vector(v.x, v.y, v.z);
}
template<class T>
void Matrix<T>::set_right(T x,T y,T z)
{
    m[0][2] = x;
    m[1][2] = y;
    m[2][2] = z;
}
template<class T>
void Matrix<T>::set_right(const Vector<T> &v)
{
    set_right_vector(v.x, v.y, v.z);
}

// Get/Set row
template<class T>
T *Matrix<T>::get_row(unsigned int p) const
{
    if (p > ROW_SIZE) return NULL;
    T *r = new T[ROW_SIZE];
    r[0] = m[p][0];
    r[1] = m[p][1];
    r[2] = m[p][2];
    r[3] = m[p][3];
    return r;
}
template<class T>
void Matrix<T>::set_row(unsigned int p,T r0,T r1,T r2,T r3)
{
    if (p > ROW_SIZE) return;
    m[p][0] = r0;
    m[p][1] = r1;
    m[p][2] = r2;
    m[p][3] = r3;
}
// Get/Set column
template<class T>
T *Matrix<T>::get_column(unsigned int p) const
{
    if (p > COLUMN_SIZE) return NULL;
    T *r = new T[COLUMN_SIZE];
    r[0] = m[0][p];
    r[1] = m[1][p];
    r[2] = m[2][p];
    r[3] = m[3][p];
    return r;
}
template<class T>
void Matrix<T>::set_column(unsigned int p,T c0,T c1,T c2,T c3)
{
    if (p > COLUMN_SIZE) return;
    m[0][p] = c0;
    m[1][p] = c1;
    m[2][p] = c2;
    m[3][p] = c3;
}
template<class T>
void Matrix<T>::set_column(unsigned int p, const Vector<T> &v,T c3)
{
    set_column(p, v.x, v.y, v.z, c3);
}

#endif // MATRIX_H

vector.h
/*
    Copyright (C) 2017 Lari Varjonen <[email protected]>

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    version 2 as published by the Free Software Foundation.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

#ifndef VECTOR_H
#define VECTOR_H

#include <math.h>


template<class T>
class Vector
{
public:
    Vector();
    Vector(T rhs);
    Vector(T x, T y, T z);
    Vector(const T (&array)[3]);

    Vector<T>& operator =(const T rhs);
    Vector<T>& operator =(const T (&rhs)[3]);
    Vector<T>& operator =(const Vector &rhs);

    Vector<T>& operator *=(const T rhs);
    Vector<T>& operator *=(const Vector &rhs);
    Vector<T> operator *(const T rhs);
    Vector<T> operator *(const Vector &rhs);

    Vector<T>& operator /=(const T rhs);
    Vector<T>& operator /=(const Vector &rhs);
    Vector<T> operator /(const T rhs);
    Vector<T> operator /(const Vector &rhs);

    Vector<T>& operator +=(const T rhs);
    Vector<T>& operator +=(const Vector &rhs);
    Vector<T> operator +(const T rhs);
    Vector<T> operator +(const Vector &rhs);

    Vector<T>& operator -=(const T rhs);
    Vector<T>& operator -=(const Vector &rhs);
    Vector<T> operator -(const T rhs);
    Vector<T> operator -(const Vector &rhs);

    bool operator ==(const T (&rhs)[3]) const;
    bool operator ==(const T rhs) const;
    bool operator ==(const Vector &rhs) const;
    bool operator !=(const T (&rhs)[3]) const;
    bool operator !=(const T rhs) const;
    bool operator !=(const Vector &rhs) const;

    T dot(const Vector &rhs) const;
    Vector<T> cross(const Vector &rhs) const;

    T length() const;
    T length_sqr() const;
    Vector<T> normalized() const;
    void normalize();

    T x, y, z;

private:
    template<class U>
    friend std::ostream &operator <<(std::ostream &, const Vector<U> &);
};

template<class T>
std::ostream& operator <<(std::ostream &os, const Vector<T> &vec)
{
    return os << "Vector " << vec.x << ", " << vec.y << ", " << vec.z << "\n";
}

template<class T>
Vector<T>::Vector()
{
    x = y = z = 0;
}

template<class T>
Vector<T>::Vector(T rhs)
{
    x = rhs;
    y = rhs;
    z = rhs;
}

template<class T>
Vector<T>::Vector(T x, T y, T z)
{
    this->x = x;
    this->y = y;
    this->z = z;
}

template<class T>
Vector<T>::Vector(const T (&array)[3])
{
    x = array[0];
    y = array[1];
    z = array[2];
}

// Basic assignment
template<class T>
Vector<T>& Vector<T>::operator =(const T (&rhs)[3])
{
    if (*this != rhs)
    {
        x = rhs[0];
        y = rhs[1];
        z = rhs[2];
    }
    return *this;
}
template<class T>
Vector<T>& Vector<T>::operator =(const T rhs)
{
    return *this = { rhs, rhs, rhs };
}
template<class T>
Vector<T>& Vector<T>::operator =(const Vector<T> &rhs)
{
    return *this = { rhs.x, rhs.y, rhs.z };
}

// Multiplication
template<class T>
Vector<T>& Vector<T>::operator *=(const T rhs)
{
    x *= rhs;
    y *= rhs;
    z *= rhs;
    return *this;
}
template<class T>
Vector<T>& Vector<T>::operator *=(const Vector &rhs)
{
    x *= rhs.x;
    y *= rhs.y;
    z *= rhs.z;
    return *this;
}
template<class T>
Vector<T> Vector<T>::operator *(const T rhs)
{
    return Vector(*this) *= rhs;
}
template<class T>
Vector<T> Vector<T>::operator *(const Vector &rhs)
{
    return Vector(*this) *= rhs;
}

// Division
template<class T>
Vector<T>& Vector<T>::operator /=(const T rhs)
{
    x /= rhs;
    y /= rhs;
    z /= rhs;
    return *this;
}
template<class T>
Vector<T>& Vector<T>::operator /=(const Vector &rhs)
{
    x /= rhs.x;
    y /= rhs.y;
    z /= rhs.z;
    return *this;
}
template<class T>
Vector<T> Vector<T>::operator /(const T rhs)
{
    return Vector(*this) /= rhs;
}
template<class T>
Vector<T> Vector<T>::operator /(const Vector &rhs)
{
    return Vector(*this) /= rhs;
}

// Addition
template<class T>
Vector<T>& Vector<T>::operator +=(const T rhs)
{
    x += rhs;
    y += rhs;
    z += rhs;
    return *this;
}
template<class T>
Vector<T>& Vector<T>::operator +=(const Vector &rhs)
{
    x += rhs.x;
    y += rhs.y;
    z += rhs.z;
    return *this;
}
template<class T>
Vector<T> Vector<T>::operator +(const T rhs)
{
    return Vector(*this) += rhs;
}
template<class T>
Vector<T> Vector<T>::operator +(const Vector &rhs)
{
    return Vector(*this) += rhs;
}

// Subtraction
template<class T>
Vector<T>& Vector<T>::operator -=(const T rhs)
{
    return *this += -rhs;
}
template<class T>
Vector<T>& Vector<T>::operator -=(const Vector &rhs)
{
    x -= rhs.x;
    y -= rhs.y;
    z -= rhs.z;
    return *this;
}
template<class T>
Vector<T> Vector<T>::operator -(const T rhs)
{
    return Vector(*this) -= rhs;
}
template<class T>
Vector<T> Vector<T>::operator -(const Vector &rhs)
{
    return Vector(*this) -= rhs;
}

// Comparison operators
template<class T>
bool Vector<T>::operator ==(const T (&rhs)[3]) const
{
    return x == rhs[0] && y == rhs[1] && z == rhs[2];
}
template<class T>
bool Vector<T>::operator ==(const T rhs) const
{
    return x == rhs && y == rhs && z == rhs;
}
template<class T>
bool Vector<T>::operator ==(const Vector &rhs) const
{
    return x == rhs.x && y == rhs.y && z == rhs.z;
}
template<class T>
bool Vector<T>::operator !=(const T (&rhs)[3]) const
{
    return !(*this == rhs);
}
template<class T>
bool Vector<T>::operator !=(const T rhs) const
{
    return !(*this == rhs);
}
template<class T>
bool Vector<T>::operator !=(const Vector &rhs) const
{
    return !(*this == rhs);
}

// Products
template<class T>
T Vector<T>::dot(const Vector &rhs) const
{
    return x * rhs.x + y * rhs.y + z * rhs.z;
}
template<class T>
Vector<T> Vector<T>::cross(const Vector &rhs) const
{
    T _x = y * rhs.z - z * rhs.y;
    T _y = z * rhs.x - x * rhs.z;
    T _z = x * rhs.y - y * rhs.x;
    return Vector(_x, _y, _z);
}

template<class T>
T Vector<T>::length() const
{
    return sqrt(x*x + y*y + z*z);
}
template<class T>
T Vector<T>::length_sqr() const
{
    return (x*x + y*y + z*z);
}
template<class T>
Vector<T> Vector<T>::normalized() const
{
    T l = length();
    Vector<T> r = 0;
    if (l == 0) return r;
    r = (*this);
    return r /= l;
}
template<class T>
void Vector<T>::normalize()
{
    T l = length();
    if (l == 0) x = y = z = 0;
    else (*this) /= l;
}

#endif // VECTOR_H

Visitor counter with PHP and MySQL, Version 2

19.09.2020

Here is a second version of the previous one. A feature has been added to this version that it will re-register the same IP address after 86400 seconds which is 24 hours.

EDIT: 25.09.2020: Improved how to fetch the total number of visitors. Added hash. Ip-addresses are not visible anymore in the database.

sqlcommand2.sql
CREATE TABLE IF NOT EXISTS `ip` (
	ip VARCHAR(100) NOT NULL
);
CREATE TABLE IF NOT EXISTS `temp` (
	ip VARCHAR(100) NOT NULL,
	date VARCHAR(100) NOT NULL,
	UNIQUE KEY unique_ip(ip)
);

phpvisitorcounter2.php
<?php
$ip_counter_value = "00000000";
$server_name = "localhost";
$user_name = "user";
$password = "password";
$db_name = "ip-addresses";

$conn = mysqli_connect($server_name, $user_name, $password, $db_name);
if($conn !== false) {
	$client_ip = htmlspecialchars($_SERVER['REMOTE_ADDR'], ENT_QUOTES);
	$client_ip = mysqli_real_escape_string($conn, $client_ip);
	$client_ip = hash("sha256", $client_ip);
	$date = mysqli_real_escape_string($conn, time());

    $sql0 = "INSERT INTO `temp`(`ip`, `date`) VALUES ('"
			. $client_ip
			. "', '"
			. $date
			. "')";
	$sql1 = "INSERT INTO `ip`(`ip`) VALUES ('*')";

	if (mysqli_query($conn, $sql0)) {
		mysqli_query($conn, $sql1);

	} else {
		$sql = "SELECT `date` FROM `temp` WHERE `ip` = '" . $client_ip . "'";
		$res = mysqli_query($conn, $sql);
		$row = $res->fetch_assoc();

		if ($date - $row["date"] >= 86400) {
			$sql = "UPDATE `temp` SET `date` = '"
					. $date
					. "' WHERE `ip` = '"
					. $client_ip
					. "'";
			mysqli_query($conn, $sql);
			mysqli_query($conn, $sql1);
		}
	}

	$sql = "SELECT COUNT(*) AS count FROM `ip`";
	$res = mysqli_query($conn, $sql);
	mysqli_close($conn);
	$data = mysqli_fetch_assoc($res);
	$row_count = $data["count"];
	$ip_counter_value = str_pad($row_count, 8, "0", STR_PAD_LEFT);
}
?>

EDIT: 25.09.2020: Code how i converted the database so that all IP addresses are hashed.

phpvisitorcounter2-convertcode.php
<?php
$server_name = "localhost";
$user_name = "user";
$password = "password";
$db_name = "ip-addresses";

$conn = mysqli_connect($server_name, $user_name, $password, $db_name);
if($conn !== false) {
	$sql = "SELECT `ip` FROM `temp`";
	$res = mysqli_query($conn, $sql);
	while ($row = mysqli_fetch_array($res, MYSQLI_NUM)) {
		$ip = $row[0];
		$ip_hash = hash("sha256", $ip);
		$sql = "UPDATE `temp` SET `ip` = '" . $ip_hash . "' WHERE `ip` = '" . $ip . "'";
		mysqli_query($conn, $sql);
	}
	$sql = "SELECT `ip` FROM `ip`";
	$res = mysqli_query($conn, $sql);
	while ($row = mysqli_fetch_array($res, MYSQLI_NUM)) {
		$ip = $row[0];
		$sql = "UPDATE `ip` SET `ip` = '*' WHERE `ip` = '" . $ip . "'";
		mysqli_query($conn, $sql);
	}
	mysqli_close($conn);
}
?>

Visitor counter with PHP and MySQL

18.09.2020

Do you remember, in the old days, many sites had those little numbers that at its finest even looked like a 7seg screens with few digits that counted the number of times that the page was loaded. Each time you reloaded the page, one more was added to that counter number what it showed.

I made exactly that, and here is the code of it and how you can get it to work on your own website too. This code also includes that it prevents users to increase the counter number by repeatedly reloading the page again and again. However, this doesn't look like a 7seg screen but is just plain numbers. You need MySQL and PHP to get this to work on your website. Next, a little guide on how to make this working. You still need at least a basic understanding of how PHP and MySQL works:

Firstly, create a MySQL user account which you plan to use with this. Remember to use strong passwords, always. Secondly, create a new database and name it ip-addresses and add basic SELECT,INSERT,UPDATE,DELETE privileges to your firstly created user to access this database. Thirdly, run the MySQL code below in the MySQL console, inside that database:

sqlcommand.sql
CREATE TABLE IF NOT EXISTS `ip-addresses` (
	ip VARCHAR(100) NOT NULL,
	UNIQUE KEY unique_ip(ip)
);

Lastly, you can put this PHP code somewhere in the code of your web pages, fill it in with the correct login information, and use a variable $ip_counter_value to show the number of visitors on your webpage.

phpvisitorcounter.php
<?php
$ip_counter_value = "00000000";
$server_name = "localhost";
$user_name = "user";
$password = "password";
$db_name = "ip-addresses";

$conn = mysqli_connect($server_name, $user_name, $password, $db_name);
if($conn !== false) {
	$client_ip = htmlspecialchars($_SERVER['REMOTE_ADDR'], ENT_QUOTES);
	$client_ip = mysqli_real_escape_string($conn, $client_ip);
	
    $sql = "INSERT INTO `" . $db_name . "`(`ip`) VALUES ('" . $client_ip . "')";
	mysqli_query($conn, $sql);
	
	$sql = "SELECT `ip` FROM `" . $db_name . "` ORDER BY `ip`";
	$res = mysqli_query($conn, $sql);
	
	$row_count = mysqli_num_rows($res);
	mysqli_close($conn);
	$ip_counter_value = str_pad($row_count, 8, "0", STR_PAD_LEFT);
}
?>

Convert F-Secure KEY exported json file to csv

07.09.2020

If you need to transfer accounts from the F-Secure KEY to the KeePassXC, this is the solution to go. The F-Secure KEY does not have the Linux support. In the Linux i'm using the KeePassXC application so i needed to move all my stored accounts from the F-Secure KEY to the KeePassXC application.

Turned out that the F-Secure KEY does not support any other format to export the accounts than json and the KeePassXC wont support json format so i needed to make this little Python code to make the convert from json to csv. Here it is if someone is in a similar situation and needs this.

fsk2csv.py
import json

if __name__ == '__main__':
	with open('ExportedPasswords.fsk', 'r', encoding='utf-8') as json_file:
		data = json.load(json_file)
		str_ = ''
		for key, value in data['data'].items():
			service =   value['service'].replace('"', '""')
			username =  value['username'].replace('"', '""')
			password =  value['password'].replace('"', '""')
			url =       value['url'].replace('"', '""')
			notes =     value['notes'].replace('"', '""')
			str_ += f'"Root","{service}","{username}","{password}","{url}","{notes}"\n'

		file = open('output.csv', 'w', encoding='utf-8')
		file.write(str_)

String sequence guesser with c++

27.05.2020

This program uses the full power of the CPU to compute the string given to it. You can adjust the CPU usage from the variables. It calculates the ASCII characters as numbers and it goes through every possibility until it finds the correct combination of characters, without knowing it beforehand. It just comparing against the correct one without knowing it. It only knows the length from the given correct character set because i don't want it to compute unlimitedly. You can download the code from this link.

It also has nice console loading bar while calculating and after it finds the correct string sequence, it prints the time for how long it took in nanoseconds as shown in the picture. 513041023200 is 8.55 minutes.

How long the process can be, it depends how fast your computer is where you run it and what is the length of the string that you give to this program. Longer strings take longer to go through.

It also matters where the given character is in the ASCII character set. If the character is at the end then it can significantly extend the processing time, because it needs to go through and try every wrong answer too between the beginning and the end, where the correct character is located(as we know, but the program does not know). It doesn't know the positions of the correct characters. It just calculates through everything until it finds the correct combination of characters.

ATTENTION! This program is for educational purposes only. I created it because i'm interested of how things works. About illegal aspects of this kind of programs, i don't take any responsibilities of how you use this program. I don't recommend to use it illegally and i don't take any responsibilities from that kind of actions.

passwordguesser.cpp
/*
Copyright(C) 2020 Lari Varjonen <[email protected]>

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
version 2 as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110 - 1301, USA.
*/

#include <boost/multiprecision/cpp_dec_float.hpp>
using boost::multiprecision::cpp_dec_float_50;

#include <chrono>
using namespace std::literals::chrono_literals;

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <atomic>

unsigned int max_simultaneous_threads = 16;
size_t max_single_thread_length = 4;
cpp_dec_float_50 biggest_possible_count_dec = 1;
cpp_dec_float_50 current_position_dec = 0;
std::atomic_bool process_is_running = true;
unsigned int active_threads = 0;
std::vector<int> password_found;
std::mutex lock;

void update_console(cpp_dec_float_50, cpp_dec_float_50);
void create_new_thread(std::vector<int>*, std::vector<int>*, std::vector<int>*);
void thread_work(std::vector<int>, std::vector<int>, std::vector<int>);
void add_one(std::vector<int>&, unsigned int);
bool is_equal(std::vector<int> const &v1, std::vector<int> const &v2)
{
	return (v1.size() == v2.size() &&
		std::equal(v1.begin(), v1.end(), v2.begin()));
}

int main(int argc, char* argv[])
{
	if (argc > 2)
	{
		max_simultaneous_threads = atoi(argv[1]);
		max_single_thread_length = atoi(argv[2]);
	}
	std::string user_passwd_str;
	if (argc > 3)
	{
		user_passwd_str = argv[3];
		for (int i = 4; i < argc; i++)
		{
			user_passwd_str += (char)32;
			user_passwd_str += argv[i];
		}
	}
	else
	{
		std::getline(std::cin, user_passwd_str);
	}
	if (user_passwd_str.size() <= max_single_thread_length)
	{
		max_single_thread_length = user_passwd_str.size()-1;
	}
	if (user_passwd_str.size() > max_single_thread_length)
	{
		for (unsigned int i = 0; i < user_passwd_str.size() - max_single_thread_length; i++)
		{
			biggest_possible_count_dec *= 223;
		}
	}
	std::vector<int> user_passwd;
	for (unsigned int i = 0; i < user_passwd_str.size(); i++)
	{
		int u = (int)user_passwd_str.at(i);
		if (u < 0)
		{
			u *= -1;
			u = 127 + (127 - u) + 2;
		}
		user_passwd.push_back(u);
	}

	std::vector<int> start;
	std::vector<int> end;
	std::vector<int> final_end;
	start.resize(user_passwd.size(), 32);
	end.resize(user_passwd.size(), 32);
	final_end.resize(user_passwd.size(), 255);

	std::chrono::high_resolution_clock::time_point t1 = std::chrono::high_resolution_clock::now();
	while(process_is_running)
	{
		if (active_threads < max_simultaneous_threads)
		{
			create_new_thread(&user_passwd, &start, &end);
			if (is_equal(end, final_end))
			{
				while (active_threads > 0 && process_is_running);
				break;
			}
			add_one(end, 0);
			start = end;
		}
	}
	std::chrono::high_resolution_clock::time_point t2 = std::chrono::high_resolution_clock::now();

	update_console(100, 2);
	std::cout << std::endl;

	std::cout << "Password is ";
	for (unsigned int i = 0; i < password_found.size(); i++)
	{
		std::cout << (char)password_found.at(i);
	}
	std::cout << std::endl;

	auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count();
	std::cout << duration << std::endl;
	
	return 0;
}

void update_console(cpp_dec_float_50 p, cpp_dec_float_50 scale)
{
	for (int i = 0; i < floor(p/scale); i++)
	{
		std::cout << (char)219;
	}
	for (int i = 0; i < 50 - floor(p/scale); i++)
	{
		std::cout << (char)176;
	}
	std::cout << (char)32 << floor(p) << "%\r";
}

void create_new_thread(std::vector<int>* user_passwd,
					   std::vector<int>* start_pos,
					   std::vector<int>* end_pos)
{
	for (unsigned int i = 0; i < max_single_thread_length; i++)
	{
		(*end_pos)[i] = 255;
	}

	current_position_dec += 1.0;
	cpp_dec_float_50 p = current_position_dec / (biggest_possible_count_dec / 100.0);
	update_console(p, 2);

	active_threads++;
	std::thread t(thread_work, *user_passwd, *start_pos, *end_pos);
	t.detach();
}

void thread_work(std::vector<int> user_passwd,
				 std::vector<int> start_pos,
				 std::vector<int> end_pos)
{
	while (process_is_running && !is_equal(start_pos, end_pos))
	{
		add_one(start_pos, 0);
		if (is_equal(start_pos, user_passwd))
		{
			std::lock_guard <std::mutex> lock(lock);
			for (unsigned int i = 0; i < start_pos.size(); i++)
			{
				password_found.push_back(start_pos.at(i));
			}
			process_is_running = false;
			break;
		}
	}
	std::lock_guard <std::mutex> lock(lock);
	active_threads--;
}

void add_one(std::vector<int> &passwd, unsigned int i)
{
	if (passwd[i] < 255)
	{
		passwd[i]++;
		if (passwd[i] == 127)
			passwd[i]++;
		return;
	}
	passwd[i] = 32;
	i++;
	if (i < passwd.size())
	{
		add_one(passwd, i);
	}
}

Discord slot machine bot (using Twitch points as coins)

07.06.2020

I had an idea to test how to make a Discord bot and here is what it became. Still i was out of ideas that what kind of Discord bot i could create. Thanks for Janne(Jenkkemi) for the idea to make this kind of bot. It was very fun to create and program this.

It uses the Discord API to handle all the Discord side stuff, and the WebSockets to handle all the Twitch side stuff. It uses the Asyncio to processing all that together. You can download the full project from this link. It is licensed with GPL v2 license only. It is free to use under the license agreements. Here are all the codes that it includes.



Main file that runs the whole program.

bot.py
"""
	Copyright (C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Thanks for Janne(Jenkkemi) for the idea to make this kind of bot: https://twitter.com/JanneKuoksa

	Version: 1.3
"""

from overload_builtins import print
import discord
import asyncio
import secret
import link_accounts
import get_channel_redemption
import commands.casino

link_accounts = link_accounts.LinkAccounts()
casino = commands.casino.Casino()


admins = {} # Admin Discord user IDs


class Client(discord.Client):
	async def on_ready(self):
		print(f'Logged on as {self.user}')
		asyncio.create_task(link_accounts.init(client))

	async def on_message(self, message):
		if message.author == self.user:
			return

		# Private messages
		if isinstance(message.channel, discord.channel.DMChannel):
			# !link
			if message.content == link_accounts.command[0]:
				if await link_accounts.link(message):
					await link_accounts.wait_user_response(message)

			# !unlink
			elif message.content == link_accounts.command[1]:
				await link_accounts.unlink(message)

			if int(message.author.id) not in admins:
				return

		# !casino
		if message.content.startswith(casino.command[0]):
			cut_casino = len(casino.command[0]) + 1
			# !casino roll
			if message.content[cut_casino:].startswith(casino.command[1]):
				await casino.identify_author_id(message)
				money_added = await link_accounts.check_cash_waiting(message, casino)
				await casino.roll(message.content[cut_casino:].split(), message, money_added)

			# !casino
			elif message.content == casino.command[0]:
				s = casino.print_rules(1)
				await message.channel.send(s)
				print(f'{message.author.id} {message.author.name}: {message.content}: print rules')

			# !casino rules
			elif message.content[cut_casino:] == casino.command[2]:
				await casino.identify_author_id(message)
				money_added = await link_accounts.check_cash_waiting(message, casino)
				await casino.print_own_rules(message, money_added)

			# !casino cash
			elif message.content[cut_casino:] == casino.command[3]:
				await casino.identify_author_id(message)
				money_added = await link_accounts.check_cash_waiting(message, casino)
				await casino.print_nice_balance(message, money_added)

			# !casino set
			elif message.content[cut_casino:].startswith(casino.command[4]):
				await casino.identify_author_id(message)
				await casino.set(message.content[cut_casino:].split(), message)

			# !casino add
			"""
			elif message.content[cut_casino:].startswith(casino.command[5]):
				await casino.identify_author_id(message)
				await casino.add(message.content[cut_casino:].split(), message)
			"""


if __name__ == '__main__':
	casino.init()
	loop = asyncio.get_event_loop()
	loop.create_task(casino.save_database_interval())
	loop.create_task(get_channel_redemption.connect(link_accounts.gcr_callback))

	client = Client()
	client.run(secret.discord_token)
	casino.save_database()

The actual game logic that makes all the game actions, like rolling the reels and get the next reel positions and so on.

casino.py
"""
	Copyright (C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Thanks for Janne(Jenkkemi) for the idea to make this kind of bot: https://twitter.com/JanneKuoksa
"""

from overload_builtins import print
from random import randint
import asyncio
import time
import math
import json
import copy

bell = ':bell:'
heart = ':crown:'
diamond = ':diamond_shape_with_a_dot_inside:'
spade = ':hearts:'
horseshoe = ':dragon:'
star = ':star:'
wild = ':moneybag:'


class Casino:
	command = ['!casino', 'roll', 'rules', 'cash', 'set', 'add']
	data_file_name = 'casino-data.json'
	roll_cost = 1
	registered_author_ids = {}
	reel = [0, 0, 0]
	interval_time_to_save = 60 * 10
	reel_ = [
		[
			bell,
			horseshoe,
			spade,
			horseshoe,
			diamond,
			horseshoe,
			spade,
			horseshoe,
			heart,
			horseshoe
		],
		[
			bell,
			horseshoe,
			spade,
			horseshoe,
			diamond,
			horseshoe,
			spade,
			horseshoe,
			heart,
			wild
		],
		[
			bell,
			diamond,
			star,
			spade,
			bell,
			diamond,
			heart,
			star,
			spade,
			diamond
		]
	]

	# Bell Heart Diamond Spade Horseshoe Star Wild
	table_to_win = [
		[[0], [8], [4], [2, 6], [1, 3, 5, 7, 9], [], []],  # Reel 1
		[[0], [8], [4], [2, 6], [1, 3, 5, 7], [], [9]],  # Reel 2
		[[0, 4], [6], [1, 5, 9], [3, 8], [], [2, 7], []]  # Reel 3
	]

	pay_table = [
		[20, table_to_win[0][0], table_to_win[1][0], [False, table_to_win[2][0]]],  # $20 = Bell Bell Bell
		[16, table_to_win[0][1], table_to_win[1][1], [False, table_to_win[2][1]]],  # $16 = Heart Heart Heart
		[12, table_to_win[0][2], table_to_win[1][2], [False, table_to_win[2][2]]],  # $12 = Diamond Diamond Diamond
		[8, table_to_win[0][3], table_to_win[1][3], [False, table_to_win[2][3]]],  # $8  = Spade Spade Spade
		[4, table_to_win[0][4], table_to_win[1][4], [False, table_to_win[2][5]]],  # $4  = Horseshoe Horseshoe Star
		[2, table_to_win[0][4], table_to_win[1][4], [True, table_to_win[2][5]]]
		# $2  = Horseshoe Horseshoe Any(not Star)
	]

	def init(self):
		try:
			with open(self.data_file_name, 'r', encoding='utf-8') as f:
				data = f.read()
				self.registered_author_ids = json.loads(data)
		except IOError:
			pass

	def save_database(self):
		with open(self.data_file_name, 'w', encoding='utf-8') as f:
			json.dump(self.registered_author_ids, f, ensure_ascii=False, indent=4)

	async def save_database_interval(self):
		registered_author_ids = copy.deepcopy(self.registered_author_ids)
		while True:
			await asyncio.sleep(self.interval_time_to_save)
			if self.registered_author_ids.items() != registered_author_ids.items():
				print('saving database')
				self.save_database()
				registered_author_ids = copy.deepcopy(self.registered_author_ids)
				print('database saved')

	async def roll(self, cut_msg, message, money_added):
		current_author_id = str(message.author.id)
		if len(cut_msg) > 4:
			return
		if len(cut_msg) > 1 and not str(cut_msg[1]).isdigit():
			return
		if len(cut_msg) > 2 and not str(cut_msg[2]).isdigit():
			return
		if len(cut_msg) > 3 and not str(cut_msg[3]).isdigit():
			return

		count = 1
		# Set auto roll count
		if len(cut_msg) > 1 and str(cut_msg[1]).isdigit() and int(cut_msg[1]) > 0:
			count = int(cut_msg[1])
			if count > 10:
				count = 10
		# Set multiplier: needs to be before set_payout_limit function
		if len(cut_msg) > 2 and str(cut_msg[2]).isdigit():
			self.set_multiplier(current_author_id, int(cut_msg[2]))
		# Set payout limit
		if len(cut_msg) > 3 and str(cut_msg[3]).isdigit():
			self.set_payout_limit(current_author_id, int(cut_msg[3]))

		# Do game
		game_str = ''
		for i in range(count):
			if self.roll_cost * self.get_multiplier(current_author_id) > self.get_balance(current_author_id):
				game_str += '\n :money_with_wings: `You don\'t have enough cash to roll`'
				break
			self.update_balance(current_author_id, -self.roll_cost * self.get_multiplier(current_author_id))
			self.reel[0] = randint(0, 9)
			self.reel[1] = randint(0, 9)
			self.reel[2] = randint(0, 9)
			game_str += f'\n{str(self.get_reels_position())}'
			current_winnings = self.check_reels_update_balance(current_author_id)
			game_str += str(self.get_nice_balance(current_author_id, current_winnings))
			if current_winnings >= int(self.get_payout_limit(current_author_id)):
				break

		str_ = f'<@{current_author_id}> '
		if money_added > 0:
			str_ += f'{self.twitch_money_added_message(message, money_added)}'
		str_ += f'{game_str}'
		await message.channel.send(str_)

	def get_reels_position(self):
		s0 = self.reel_[0][self.reel[0]]
		s1 = self.reel_[1][self.reel[1]]
		s2 = self.reel_[2][self.reel[2]]
		return s0 + s1 + s2

	def check_reels_update_balance(self, current_author_id):
		current_winnings = 0
		for i in range(len(self.pay_table)):
			# Reel 1 check
			if self.reel[0] in self.pay_table[i][1]:
				# Reel 2 check, and if WILD then just pass through
				if self.reel[1] in self.table_to_win[1][6] \
						or self.reel[1] in self.pay_table[i][2]:
					# Reel 3 check and handling Any(not Star) case
					if not self.pay_table[i][3][0]:
						if self.reel[2] in self.pay_table[i][3][1]:
							current_winnings = self.pay_table[i][0] * self.get_multiplier(current_author_id)
							self.update_balance(current_author_id, current_winnings)
					else:
						if self.reel[2] not in self.pay_table[i][3][1]:
							current_winnings = self.pay_table[i][0] * self.get_multiplier(current_author_id)
							self.update_balance(current_author_id, current_winnings)
			# Stop point handling
			if current_winnings >= int(self.get_payout_limit(current_author_id)):
				break

		return current_winnings

	def update_balance(self, current_author_id, i: int):
		self.registered_author_ids[current_author_id][0] += i

	def get_nice_balance(self, current_author_id, current_winnings=0):
		s = f'`${self.registered_author_ids[current_author_id][0]}`'
		s += f' `M:{self.get_multiplier(current_author_id)}`'
		s += f' `L:{self.get_payout_limit(current_author_id)}`'
		if current_winnings > 0:
			s += f' `You won! ${current_winnings}`'
		return s

	async def print_nice_balance(self, message, money_added):
		current_author_id = str(message.author.id)
		str_ = f'{self.get_nice_balance(current_author_id)}'
		if money_added > 0:
			str_ = f'{self.twitch_money_added_message(message, money_added)}\n{str_}'
		await message.channel.send(f'<@{current_author_id}> {str_}')

	def get_balance(self, current_author_id):
		return self.registered_author_ids[current_author_id][0]

	def get_multiplier(self, current_author_id):
		return self.registered_author_ids[current_author_id][1]

	def set_multiplier(self, current_author_id, i):
		if i < 1:
			i = 1
		elif i > 4:
			i = 4
		self.registered_author_ids[current_author_id][1] = i

	def get_payout_limit(self, current_author_id):
		return self.registered_author_ids[current_author_id][2]

	def set_payout_limit(self, current_author_id, i):
		maximum_payout = self.pay_table[0][0] * self.get_multiplier(current_author_id)
		smallest_payout = self.pay_table[len(self.pay_table) - 1][0] * self.get_multiplier(current_author_id)
		if i < smallest_payout:
			i = smallest_payout  # Smallest possible payout
		elif i > maximum_payout:
			i = maximum_payout  # Maximum possible payout
		self.registered_author_ids[current_author_id][2] = i

	async def identify_author_id(self, message):
		current_author_id = str(message.author.id)
		# # Add new user
		if current_author_id not in self.registered_author_ids:
			# [balance, multiplier, stop if this or greater payout reached, current time]
			self.registered_author_ids[current_author_id] = [20, 1, 20, time.time()]
			self.save_database()

		str_ = f'{message.author.id} {message.author.name}: {message.content}: '
		str_ += self.get_nice_balance(current_author_id).replace('`', '')
		print(str_)

	async def print_own_rules(self, message, money_added):
		print(f'{message.author.id} {message.author.name}: {message.content}: print own rules')
		current_author_id = str(message.author.id)
		str_ = f'{self.get_nice_balance(current_author_id)}\n'
		str_ += f'{self.print_rules(self.get_multiplier(current_author_id))}'
		if money_added > 0:
			str_ = f'{self.twitch_money_added_message(message, money_added)}\n{str_}'
		await message.channel.send(f'<@{current_author_id}> {str_}')

	def print_rules(self, multiplier=1):
		s = ''
		for i in range(len(self.pay_table) - 1):
			s += self.reel_[0][self.pay_table[i][1][0]]
			s += self.reel_[1][self.pay_table[i][2][0]]
			s += self.reel_[2][self.pay_table[i][3][1][0]]
			s += f' `${self.pay_table[i][0] * multiplier}`\n'
		s += f'{horseshoe}{horseshoe} Any(Not {star}) `${2 * multiplier}`\n\n'

		s += f'{wild} **WILD** symbol on the center reel.'
		s += ' Wild can substitute for any other symbol, with no exceptions.\n\n'

		s += '**Get more money!** Redeem specific Twitch reward from'
        s += ' [twitch channel name here] Twitch channel using your channel points'
		s += ' to get more money. Link your Discord and Twitch account first by sending `!link`'
		s += ' private message for this bot. You can also unlink your Discord and Twitch connection by'
		s += ' sending `!unlink` private message for this bot.\n\n'

		s += '`!casino rules` or `!casino` Shows you the casino rules.\n'
		s += '`!casino roll` Roll the reels once.\n'
		s += '`!casino roll 10` Roll the reels multiple times at once. Number can be between 1 - 10.\n'
		s += '`!casino roll 10 4` Set the multiplier. Number can be between 1 - 4.\n'
		s += '`!casino roll 10 4 80` Set the payout limit. Stop if this or greater payout.\n'
		s += '`!casino cash` Show how much money you have at the moment.\n'
		s += '`!casino set M` Set the multiplier. M can also be m, mult, multp or multiplier.\n'
		s += '`!casino set L` Set the payout limit. L can also be l, limit or payoutlimit.\n'
		# s += '`!casino add M` Add $100 money. Every 10 minutes you can add money. M can also be m or money.'
		return s

	async def set(self, cut_msg, message):
		current_author_id = str(message.author.id)

		print(f'{message.author.id} {message.author.name}: {message.content}')

		if len(cut_msg) < 3 or len(cut_msg) > 3:
			return

		str_ = ''
		# Set Multiplier
		if cut_msg[1] in ['m', 'M', 'mult', 'multp', 'multiplier'] and str(cut_msg[2]).isdigit():
			self.set_multiplier(current_author_id, int(cut_msg[2]))
			str_ = f' `Multiplier set to {self.get_multiplier(current_author_id)}`'

		# Set stop point
		elif cut_msg[1] in ['l', 'L', 'limit', 'payoutlimit'] and str(cut_msg[2]).isdigit():
			self.set_payout_limit(current_author_id, int(cut_msg[2]))
			str_ = f' `Payout limit set to {self.get_payout_limit(current_author_id)}`'

		if str_ == '':
			return

		await message.channel.send(f'<@{current_author_id}>{str_}')
		print(f'{message.author.id} {message.author.name}: {message.content}:{str_.lower().replace("`", "")}')

	async def add(self, cut_msg, message):
		current_author_id = str(message.author.id)

		print(f'{message.author.id} {message.author.name}: {message.content}')

		if len(cut_msg) < 2:
			return
		if not cut_msg[1] in ['m', 'M', 'money']:
			return

		# Add money
		if self.permission_to_add_100(current_author_id):
			self.update_timer_time(current_author_id)
			await self.send_money_added_message(message)
			self.save_database()
			return

		p = time.time() - self.registered_author_ids[current_author_id][3]
		m = math.floor(((60.0 * 10.0) - p) / 60.0)
		s = math.floor(((60.0 * 10.0) - p) - (m * 60))
		str_ = f' :money_with_wings: `You have to wait {m} minutes and {s} seconds before you can add more money.`'

		await message.channel.send(f'<@{current_author_id}>{str_}')

	async def send_money_added_message(self, message):
		current_author_id = str(message.author.id)
		str_ = f' :money_mouth: `Money added! Your current balance: ${self.get_balance(current_author_id)}`'
		await message.channel.send(f'<@{current_author_id}>{str_}')
		str_ = f'{message.author.id} {message.author.name}: {message.content}: '
		str_ += f'money added, current balance {self.get_balance(current_author_id)}'
		print(str_)

	def twitch_money_added_message(self, message, cost):
		current_author_id = str(message.author.id)

		str_ = f'{message.author.id} {message.author.name}: {message.content}: '
		str_ += f'twitch money added {cost}, current balance {self.get_balance(current_author_id)}'
		print(str_)

		str_ = f':money_mouth: `You redeemed a Twitch reward and gained ${cost} of cash! Your current balance:'
		str_ += f' ${self.get_balance(current_author_id)}`'

		return str_

	def permission_to_add_100(self, current_author_id):
		if time.time() - self.registered_author_ids[current_author_id][3] > 60 * 10:
			self.update_balance(current_author_id, 100)
			return True
		return False

	def update_timer_time(self, current_author_id):
		self.registered_author_ids[current_author_id][3] = time.time()

This is the key file between Discord and Twitch. It handels the Discord and Twitch accounts linking process. If the user asks to link the Discord and Twitch accounts, the bot asks the user to send a generated unique identifier ID through Twitch, back to the bot, that it can know that this user have this Twitch account, and so link those together. Partly it also handles the action of users to redeem a specific reward from Twitch account to add more money to the bot cash for that specific user that asked it.

It also handles the situation if someone redeems the specific reward in Twitch to add more money, but the user doesn't have lineked the Discord and Twitch accounts yet, and the bot does not know yet to from which user this is from. It saves the reward redeem action and before every user gaming action it checks the user from remeeded rewards, if the user had linked the accounts and if it have some rewards waiting. It handles also the timing how users can whisper at the same time to the bot, while keeping their own time limits active.

link_accounts.py
"""
	Copyright (C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Thanks for Janne(Jenkkemi) for the idea to make this kind of bot: https://twitter.com/JanneKuoksa
"""

from overload_builtins import print
import time
import asyncio
import json

import nonce
import get_whispers


class LinkAccounts:
	command = ['!link', '!unlink']
	linked_accounts_fn = 'linked-account.json'
	cash_waiting_fn = 'cash-waiting-requests.json'
	linked_accounts = {}
	expect_response = {}
	cash_waiting = {}
	dc_client = None
	get_whispers = None
	init_done = False

	async def init(self, dc_client):
		if self.init_done:
			print('init already done')
			return
		self.init_done = True
		self.dc_client = dc_client
		await self.load_linked_accounts()
		await self.load_cash_waiting()
		self.get_whispers = get_whispers.WebSocketClient(self.gw_callback)

	async def load_linked_accounts(self):
		try:
			with open(self.linked_accounts_fn, 'r', encoding='utf-8') as f:
				data = f.read()
				self.linked_accounts = json.loads(data)
		except IOError:
			pass

	def save_linked_accounts(self):
		with open(self.linked_accounts_fn, 'w', encoding='utf-8') as f:
			json.dump(self.linked_accounts, f, ensure_ascii=False, indent=4)

	async def load_cash_waiting(self):
		try:
			with open(self.cash_waiting_fn, 'r', encoding='utf-8') as f:
				data = f.read()
				self.cash_waiting = json.loads(data)
		except IOError:
			pass

	def save_cash_waiting(self):
		with open(self.cash_waiting_fn, 'w', encoding='utf-8') as f:
			json.dump(self.cash_waiting, f, ensure_ascii=False, indent=4)

	async def gcr_callback(self, display_name: str, tw_uid: str, cost: int):
		print(f'{display_name} {tw_uid} {cost}')

		if tw_uid in self.cash_waiting:
			self.cash_waiting[tw_uid] += cost
		else:
			self.cash_waiting[tw_uid] = cost
		self.save_cash_waiting()

		for dc_uid, tw_uid_ in self.linked_accounts.items():
			if tw_uid_ == tw_uid:
				dc_user = self.dc_client.get_user(int(dc_uid))
				await dc_user.send(f'Successfully transferred {cost} Twitch channel points to your '
								   f'slot machine cash.')
				return

	async def gw_callback(self, tw_uid: str, received_token: str):
		print(f'{tw_uid} {received_token}')
		if received_token in self.expect_response:
			print('expected token received. user added to the linked accounts')
			self.linked_accounts[self.expect_response[received_token]] = str(tw_uid)
			del self.expect_response[received_token]
			if not self.expect_response and self.get_whispers.connection.open:
				await self.get_whispers.connection.close()
				await self.get_whispers.connection.wait_closed()
			self.save_linked_accounts()
		else:
			print('expected token NOT received')

	async def check_cash_waiting(self, message, casino):
		message_author_id = str(message.author.id)

		if message_author_id not in self.linked_accounts:
			return 0

		for tw_uid, cost in self.cash_waiting.items():
			if tw_uid == self.linked_accounts[message_author_id]:

				print(f'casino add {cost}')

				# Add new twitch reward money to user current balance
				casino.update_balance(message_author_id, int(cost))
				casino.save_database()

				del self.cash_waiting[tw_uid]
				self.save_cash_waiting()

				return int(cost)
		return 0

	async def unlink(self, message):
		str_ = f'{message.author.id} {message.author.name}: {message.content}:'
		current_author_id = str(message.author.id)
		if current_author_id in self.linked_accounts:
			del self.linked_accounts[current_author_id]
			print(f'{str_} account successfully unlinked')
			await message.channel.send('Account successfully unlinked.')
			self.save_linked_accounts()
			return
		print(f'{str_} your account is not linked')
		await message.channel.send('Your account is not linked.')

	async def link(self, message):
		str_ = f'{message.author.id} {message.author.name}: {message.content}:'
		current_author_id = str(message.author.id)
		if current_author_id in self.linked_accounts:
			print(f'{str_} account already linked')
			await message.channel.send('Account already linked.')
			return 0
		if current_author_id in self.expect_response.values():
			print(f'{str_} the link process is already running for this user')
			return 0
		else:
			print(f'{str_} link')

		generated_token = nonce.get(16)
		while generated_token in self.expect_response:
			generated_token = nonce.get(16)

		self.expect_response[generated_token] = current_author_id

		await self.get_whispers.update_time()
		if self.get_whispers.connection is None or not self.get_whispers.connection.open:
			await self.get_whispers.connect()
			asyncio.create_task(self.get_whispers.heartbeat())
			asyncio.create_task(self.get_whispers.on_message())
			asyncio.create_task(self.get_whispers.timer())

		s = 'In order to complete your linking process please whisper '
		s += 'in Twitch within 1 minute. Copy this: '
		s += f'`/w {get_whispers.bot_name} {generated_token}`'
		await message.channel.send(s)
		return 1

	async def wait_user_response(self, message):
		str_ = f'{message.author.id} {message.author.name}: {message.content}:'
		current_author_id = str(message.author.id)
		for key, value in self.expect_response.items():
			if value == current_author_id:
				print(f'{str_} waiting for user response: {key}')
				break
		time_ = self.get_whispers.time
		while True:
			await asyncio.sleep(1)
			# If response is expect response
			if current_author_id in self.linked_accounts:
				print(f'{str_} successfully linked account')
				await message.channel.send('You have successfully linked your Discord and Twitch accounts.')
				return

			# If waiting time ends
			if time.time() > time_:
				print(f'{str_} time ends')
				# Remove expect response from the expect_response dict
				for key, value in self.expect_response.items():
					if value == current_author_id:
						del self.expect_response[key]
						break
				break
		await message.channel.send('Time limit reached. Try again: `!link`')
		print(f'{str_} try again')

While user wants to connect the Discord and Twitch channels, this receives the user messages from Twitch and keeping the connection alive until all time limits are reached from all active users or when also the last active user completes the assignment of sending the identifier ID to the bot through Twitch successfully.

get_whispers.py
"""
	Copyright (C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Thanks for Janne(Jenkkemi) for the idea to make this kind of bot: https://twitter.com/JanneKuoksa
"""

from overload_builtins import print
import asyncio
import websockets
import random
import json
import time

import nonce
import secret

bot_name = 'bot_name'


class WebSocketClient:
	time = 0
	connection = None

	def __init__(self, callback_function):
		self.topics = ['whispers.[CHANNEL ID]']
		self.uri = 'wss://pubsub-edge.twitch.tv'
		self.callback_function = callback_function
		self.connection = None

	async def connect(self):
		print('connect')
		self.connection = await websockets.client.connect(self.uri)
		data = {
			'type': 'LISTEN',
			'nonce': nonce.get(15),
			'data': {
					'topics': self.topics,
				'auth_token': secret.twitch_client_oauth
			}
		}
		await self.connection.send(str(data).replace('\'', '"'))

	async def heartbeat(self):
		print('starts')
		while True:
			try:
				if self.connection.open:
					await self.connection.send('{"type":"PING"}')
				else:
					print(f'self.connection.open: False')
			except Exception as e:
				print(f'Exception: {e}')
				return

			i = 120 + random.randint(0, 180)
			while i > 0:
				if not self.connection.open:
					print(f'ended')
					return
				await asyncio.sleep(1)
				i -= 1

	async def on_message(self):
		print('starts')
		while True:
			try:
				r = await self.connection.recv()
			except Exception:
				print(f'ended')
				return
			data = json.loads(r)

			if 'type' in data and data['type'] == 'PONG':
				continue

			print(r)

			if 'type' in data and data['type'] != 'MESSAGE':
				continue

			data['data']['message'] = json.loads(str(data['data']['message']))

			if data['data']['message']['type'] != 'whisper_received':
				continue

			# Get whisper sender twitch id and message
			from_id = str(data['data']['message']['data_object']['from_id'])
			body = str(data['data']['message']['data_object']['body'])
			await self.callback_function(from_id, body)

	async def get_time(self):
		return self.time

	async def update_time(self):
		print('time update')
		self.time = time.time() + 60.0  # + is how long one session can last

	async def timer(self):
		print('starts')
		while True:
			if time.time() > self.time:
				print('time ended')
				await self.connection.close()
				await self.connection.wait_closed()
				return
			if not self.connection.open:
				print('ended')
				return
			await asyncio.sleep(1)

When a user redeems the specific reward from the Twitch channel, this receive and parses the message from Twitch. This should always be running when the bot is on.

get_channel_redemption.py
"""
	Copyright (C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Thanks for Janne(Jenkkemi) for the idea to make this kind of bot: https://twitter.com/JanneKuoksa
"""

from overload_builtins import print
import asyncio
import websockets
import random
import json
import requests
import time

import nonce
import secret

custom_reward_names = ['custom_reward_names']


class WebSocketClient:
	def __init__(self, callback_function):
		self.callback_function = callback_function
		# self.channel = '[CHANNEL NAME]'
		self.topics = ['channel-points-channel-v1.[CHANNEL ID]']
		self.uri = 'wss://pubsub-edge.twitch.tv'
		self.error = False
		self.data_file_name = 'secret.json'
		self.connection = None

	async def connect(self):
		if self.connection is not None:
			if self.connection.open:
				return
		database_data = await self.update_database()
		"""
		for i in range(len(self.topics)):
			self.topics[i] = f'{self.topics[i]}.{database_data["channel_id"]}'
		"""
		self.connection = await websockets.client.connect(self.uri)
		data = {
			'type': 'LISTEN',
			'nonce': nonce.get(15),
			'data': {
				'topics': self.topics,
				'auth_token': database_data['access_token']
			}
		}
		await self.connection.send(str(data).replace('\'', '"'))

	async def heartbeat(self):
		while True:
			try:
				if self.connection.open:
					await self.connection.send('{"type":"PING"}')
				else:
					print(f'self.connection.open: False')
			except Exception as e:
				print(e)
				pass
			await asyncio.sleep(180 + random.randint(0, 120))

	async def on_message(self):
		while True:
			try:
				r = await self.connection.recv()
			except Exception:
				print('r = await self.connection.recv() FAILED')
				await self.connect()
				continue
			data = json.loads(r)

			if 'type' in data and data['type'] == 'PONG':
				continue

			if 'type' in data and data['type'] == 'RECONNECT':
				print('RECONNECT')
				await self.connection.close()
				await self.connection.wait_closed()
				await asyncio.sleep(5)
				await self.connect()
				continue

			if 'error' in data and data['error'] == 'ERR_BADAUTH':
				print('ERR_BADAUTH')
				await asyncio.sleep(5)
				self.error = True
				await self.connect()
				continue

			if 'type' in data and data['type'] != 'MESSAGE':
				continue

			data['data']['message'] = json.loads(str(data['data']['message']))

			if data['data']['message']['type'] != 'reward-redeemed':
				continue

			if not data['data']['message']['data']['redemption']['reward']['title'] in custom_reward_names:
				continue

			print(r)

			display_name = data['data']['message']['data']['redemption']['user']['display_name']
			user_id = data['data']['message']['data']['redemption']['user']['id']
			cost = data['data']['message']['data']['redemption']['reward']['cost']
			await self.callback_function(str(display_name), str(user_id), int(cost))

	async def update_database(self):
		# Load current database as data
		with open(self.data_file_name, 'r') as file:
			s = file.read()
		data = json.loads(s)

		# If token is not expired
		time_stamp = float(data['time_stamp'])
		expires_in = float(data['expires_in'])
		if self.error is False and time.time() < time_stamp + expires_in:
			return data

		self.error = False

		await asyncio.sleep(1)

		# Get new tokens
		params = {
			'grant_type': 'refresh_token',
			'refresh_token': bytes.fromhex(str(data['refresh_token'])).decode(encoding='utf_8'),
			'client_id': secret.twitch_client_id,
			'client_secret': secret.twitch_client_secret,
			'scope': data['scope'],
			'state': nonce.get(32)
		}

		url = 'https://id.twitch.tv/oauth2/token'
		r = requests.post(url, params=params)
		data = json.loads(r.text)

		# Add other data
		data['refresh_token'] = str(data['refresh_token']).encode(encoding='utf_8').hex()
		data['time_stamp'] = time.time()

		# Get channel id
		"""
		await asyncio.sleep(1)
		params = {
			'login': self.channel
		}
		headers = {
			'Authorization': f'Bearer {secret.twitch_client_oauth}',
			'Client-ID': secret.twitch_client_oauth_id,
			'Accept': 'application/vnd.twitchtv.v5+json'
		}
		url = 'https://api.twitch.tv/helix/users'
		r = requests.get(url, headers=headers, params=params)
		channel_data = json.loads(r.text)
		data['channel_id'] = channel_data['data'][0]['id']
		"""

		# Save new updated database
		with open(self.data_file_name, 'w', encoding='utf-8') as file:
			json.dump(data, file, ensure_ascii=False, indent=4)

		return data


async def connect(callback_function):
	client = WebSocketClient(callback_function)
	await client.connect()
	asyncio.create_task(client.heartbeat())
	asyncio.create_task(client.on_message())

Get nice random string of characters. This generates the identifier IDs.

nonce.py
"""
	Copyright (C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Thanks for Janne(Jenkkemi) for the idea to make this kind of bot: https://twitter.com/JanneKuoksa
"""

import random


def get(length):
	letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
	return ''.join(random.choice(letters) for i in range(length))

Only for getting a nice readable and understandable log files. If some errors happen, it is easy to see where it is coming from when all print functions is going through this.

overload_builtins.py
"""
	Copyright (C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Thanks for Janne(Jenkkemi) for the idea to make this kind of bot: https://twitter.com/JanneKuoksa
"""

import builtins as __builtin__
import datetime
import inspect
import os


def print(msg):
	frame = inspect.stack()[1]
	module = inspect.getmodule(frame[0])
	caller_file_name = os.path.basename(module.__file__)
	caller_function_name = inspect.stack()[1][3]
	datetime_now = datetime.datetime.now()
	return __builtin__.print(f'{datetime_now}: {caller_file_name}: {caller_function_name}: {msg}')

Serverbot using logfiles with Bash

11.04.2020

I wrote a chatbot program with bash. It was running on this Sharkdoom cooperative server of mine. I disabled it because I updated the server WAD collection. The current features of Chatbot were no longer needed and therefore useless. It worked by typing to the chat with forward slash character.

Everyone was able to use these commands:

  • /server do fastweapons Keeps sv_fast_weapons 1 next 30 minutes. Also it informs that one minute is remaining before time ends. It is 1 instead of 2, because 2 seems to causing the server to crash when certain situations occur.
  • /server do hello It is just a test command but i leave it there anyway. It says hello once it starts and then it waits one minute and after that it say hello again.
  • /server do list Lists these commands.

You can download the code from this link. It is licensed with GPL v2 license only. It is free to use under the license agreements. If you wanna use this program without any modifications, it requires that you run doom server using screen and -L -Logfile ./logfile.log -command-line options, example like this screen -S doomserver -L -Logfile ./logfile.log ./run.sh where run.sh file includes all server parameters and runs the server. Remember to add logfile -filepath and sname -screen sessionname to the code right variables.

serverbot_2.6.sh
#! /bin/bash

#	Copyright (C) 2020 Lari Varjonen <[email protected]>
#
#	This program is free software; you can redistribute it and/or
#	modify it under the terms of the GNU General Public License
#	version 2 as published by the Free Software Foundation.
#
#	This program is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#	GNU General Public License for more details.
#
#	You should have received a copy of the GNU General Public License
#	along with this program; if not, write to the Free Software
#	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

# Doom 2 chatbot | Version: 2.6 | Created by Lark3ri https://www.lark3ri.fi


botname="\ctSERVERBOT"
logfile="/doom2/logfile.log"
sname="screen-sessionname"
sleep_sec=2 # How long in seconds it sleeps after one valid command have been received and handled.
sleep_pid=0 # No need to change this. Just stores the sleep time process id.
pida=()

server_say() {
	screen -S "$sname" -p 0 -X stuff "say $botname\c-: $1^M"
}

server_exec_command() {
	screen -S "$sname" -p 0 -X stuff "$1^M"
}

server_sleep() {
	for i in $(seq 1 "$1");
	do
		sleep 1m
		if [ -z "$2" ]; then continue; fi
		if [[ "$i" == "$3" ]]
		then
			case "$2" in
				*FASTWEAPONS*) server_say "\cgsv_fastweapons 1 \c-one minute remaining!" ;;
			esac
		fi
	done
}

is_process_running() {
	echo "$1"
	for i in "${!pida[@]}"
	do
		pname=$(echo "${pida[$i]}" | cut -d ':' -f 1)
		if [[ "$pname" == "$1" ]]
		then
			pid=$(echo "${pida[$i]}" | cut -d ':' -f 2)
			if ps -p "$pid" > /dev/null 2>&1
			then
				case "$1" in
					*LIST*) server_say "currently list command has 10min cooldown" ;;
					*) server_say "already running" ;;
				esac
				return 0
			fi
			unset "pida[$i]"
			break
		fi
	done
	return 1
}

tail -n 0 -F "$logfile" | \
while read line
do
	if ! echo "$line" | grep -iq "^CHAT.*$"; then continue; fi
	echo "$line"
	if echo "$line" | grep -iq "^CHAT <server>:.*$"; then continue; fi
	msg=$(echo "$line" | awk -F ": " '{print $NF}' | tr a-z A-Z)
	if echo "$msg" | grep -iq "^/SERVER DO.*$"
	then
		cmd=$(echo "$msg" | cut -d ' ' -f 3 | tr a-z A-Z)
		if [ -z "$cmd" ]; then continue; fi
		if ps -p "$sleep_pid" > /dev/null 2>&1; then echo "too fast!"; continue; fi
		(sleep "$sleep_sec") &
		sleep_pid="$!"
		case "$cmd" in
			*FASTWEAPONS*)
				if is_process_running "FASTWEAPONS"; then continue; fi
				(server_exec_command "sv_fastweapons 1"; \
				 server_say "\cgsv_fastweapons 1 \c-next 30 minutes"; \
				 server_sleep 30 "FASTWEAPONS" 29; \
				 server_exec_command "sv_fastweapons 0"; \
				 server_say "\cgsv_fastweapons 1 \c-time ended") &
				pida+=("FASTWEAPONS:$!")
				;;
			*HELLO*)
				if is_process_running "HELLO"; then continue; fi
				(server_say "hi! this takes one minute..."; \
				 server_sleep 1; \
				 server_say "hello again! now it's done!") &
				pida+=("HELLO:$!")
				;;
			*LIST*)
				if is_process_running "LIST"; then continue; fi
				(server_say "\ctplayer usable commands:"; \
				 server_say "\cr/server do fastweapons \cd30min fastweapons 1"; \
				 server_say "\cr/server do hello \cdjust a test command but i leave it there anyway :)"; \
				 server_say "\cr/server do list \cdsays this message chain with 10min cooldown"; \
				 server_say "\c*more about this chatbot is in the server website https://sharkdoom.k3r.fi"; \
				 server_sleep 10) &
				pida+=("LIST:$!")
				;;
			*) server_say "No command found" ;;
		esac
	fi
done
« Back | ↑ Top